From 7f363e4141981f728dc60a5c769c72263e75548b Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 8 Apr 2024 17:05:41 +0200 Subject: [PATCH] Split off functional contact tools into its own crate I would like to implement https://github.com/deltachat/deltachat-core-rust/issues/5422 in its own crate, but it will depend on some functions that are in the `deltachat` crate. So, this PR extracts these functions into its own crate so that I can add https://github.com/deltachat/deltachat-core-rust/issues/5422 into the new crate. --- Cargo.lock | 11 ++ Cargo.toml | 16 +- deltachat-contact-utils/Cargo.toml | 18 ++ deltachat-contact-utils/src/lib.rs | 280 +++++++++++++++++++++++++++++ src/authres.rs | 2 +- src/chat.rs | 5 +- src/config.rs | 2 +- src/configure.rs | 5 +- src/contact.rs | 140 +-------------- src/decrypt.rs | 2 +- src/imap.rs | 3 +- src/imex.rs | 2 +- src/key.rs | 3 +- src/mimefactory.rs | 3 +- src/mimeparser.rs | 6 +- src/peerstate.rs | 3 +- src/pgp.rs | 2 +- src/provider.rs | 2 +- src/qr.rs | 5 +- src/qr/dclogin_scheme.rs | 6 +- src/reaction.rs | 4 +- src/receive_imf.rs | 11 +- src/scheduler/connectivity.rs | 6 +- src/securejoin.rs | 5 +- src/sql/migrations.rs | 2 +- src/test_utils.rs | 4 +- src/tools.rs | 117 +----------- src/webxdc.rs | 2 +- 28 files changed, 378 insertions(+), 289 deletions(-) create mode 100644 deltachat-contact-utils/Cargo.toml create mode 100644 deltachat-contact-utils/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1d380b9e0e..a60782427b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,6 +1111,7 @@ dependencies = [ "brotli", "chrono", "criterion", + "deltachat-contact-utils", "deltachat-time", "deltachat_derive", "email", @@ -1176,6 +1177,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "deltachat-contact-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "once_cell", + "regex", + "rusqlite", +] + [[package]] name = "deltachat-jsonrpc" version = "1.137.2" diff --git a/Cargo.toml b/Cargo.toml index d657a5c4e0..ff965bf337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,11 @@ strip = true [dependencies] deltachat_derive = { path = "./deltachat_derive" } deltachat-time = { path = "./deltachat-time" } +deltachat-contact-utils = { path = "./deltachat-contact-utils" } format-flowed = { path = "./format-flowed" } ratelimit = { path = "./deltachat-ratelimit" } -anyhow = "1" +anyhow = { workspace = true } async-channel = "2.0.0" async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] } async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] } @@ -67,7 +68,7 @@ mime = "0.3.17" num_cpus = "1.16" num-derive = "0.4" num-traits = "0.2" -once_cell = "1.18.0" +once_cell = { workspace = true } percent-encoding = "2.3" parking_lot = "0.12" pgp = { version = "0.11", default-features = false } @@ -77,9 +78,9 @@ qrcodegen = "1.7.0" quick-xml = "0.31" quoted_printable = "0.5" rand = "0.8" -regex = "1.10" +regex = { workspace = true } reqwest = { version = "0.12.2", features = ["json"] } -rusqlite = { version = "0.31", features = ["sqlcipher"] } +rusqlite = { workspace = true, features = ["sqlcipher"] } rust-hsluv = "0.1" sanitize-filename = "0.5" serde_json = "1" @@ -132,6 +133,7 @@ members = [ "deltachat-repl", "deltachat-time", "format-flowed", + "deltachat-contact-utils", ] [[bench]] @@ -162,6 +164,12 @@ harness = false name = "send_events" harness = false +[workspace.dependencies] +anyhow = "1" +once_cell = "1.18.0" +regex = "1.10" +rusqlite = { version = "0.31" } + [features] default = ["vendored"] internals = [] diff --git a/deltachat-contact-utils/Cargo.toml b/deltachat-contact-utils/Cargo.toml new file mode 100644 index 0000000000..9a11b8e272 --- /dev/null +++ b/deltachat-contact-utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "deltachat-contact-utils" +version = "0.1.0" +edition = "2021" +description = "Contact-related tools, like parsing vcards and sanitizing name and address" +license = "MPL-2.0" +# TODO maybe it should be called "deltachat-text-utils" or similar? + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +once_cell = { workspace = true } +regex = { workspace = true } +rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature. + +[dev-dependencies] +anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests. diff --git a/deltachat-contact-utils/src/lib.rs b/deltachat-contact-utils/src/lib.rs new file mode 100644 index 0000000000..a34938e1ac --- /dev/null +++ b/deltachat-contact-utils/src/lib.rs @@ -0,0 +1,280 @@ +//! Contact-related tools, like parsing vcards and sanitizing name and address + +#![forbid(unsafe_code)] +#![warn( + unused, + clippy::correctness, + missing_debug_implementations, + missing_docs, + clippy::all, + clippy::wildcard_imports, + clippy::needless_borrow, + clippy::cast_lossless, + clippy::unused_async, + clippy::explicit_iter_loop, + clippy::explicit_into_iter_loop, + clippy::cloned_instead_of_copied +)] +#![cfg_attr(not(test), warn(clippy::indexing_slicing))] +#![allow( + clippy::match_bool, + clippy::mixed_read_write_in_expression, + clippy::bool_assert_comparison, + clippy::manual_split_once, + clippy::format_push_string, + clippy::bool_to_int_with_if +)] + +use std::fmt; +use std::ops::Deref; + +use anyhow::bail; +use anyhow::Result; +use once_cell::sync::Lazy; +use regex::Regex; + +/// Valid contact address. +#[derive(Debug, Clone)] +pub struct ContactAddress(String); + +impl Deref for ContactAddress { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for ContactAddress { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for ContactAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ContactAddress { + /// Constructs a new contact address from string, + /// normalizing and validating it. + pub fn new(s: &str) -> Result { + let addr = addr_normalize(s); + if !may_be_valid_addr(&addr) { + bail!("invalid address {:?}", s); + } + Ok(Self(addr.to_string())) + } +} + +/// Allow converting [`ContactAddress`] to an SQLite type. +impl rusqlite::types::ToSql for ContactAddress { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Text(self.0.to_string()); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +/// Make the name and address +pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) { + static ADDR_WITH_NAME_REGEX: Lazy = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap()); + if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) { + ( + if name.is_empty() { + strip_rtlo_characters( + &captures + .get(1) + .map_or("".to_string(), |m| normalize_name(m.as_str())), + ) + } else { + strip_rtlo_characters(name) + }, + captures + .get(2) + .map_or("".to_string(), |m| m.as_str().to_string()), + ) + } else { + (strip_rtlo_characters(name), addr.to_string()) + } +} + +/// Normalize a name. +/// +/// - Remove quotes (come from some bad MUA implementations) +/// - Trims the resulting string +/// +/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`. +pub fn normalize_name(full_name: &str) -> String { + let full_name = full_name.trim(); + if full_name.is_empty() { + return full_name.into(); + } + + match full_name.as_bytes() { + [b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name + .get(1..full_name.len() - 1) + .map_or("".to_string(), |s| s.trim().to_string()), + _ => full_name.to_string(), + } +} + +const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}']; +/// This method strips all occurrences of the RTLO Unicode character. +/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)? +pub fn strip_rtlo_characters(input_str: &str) -> String { + input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "") +} + +/// Returns false if addr is an invalid address, otherwise true. +pub fn may_be_valid_addr(addr: &str) -> bool { + let res = EmailAddress::new(addr); + res.is_ok() +} + +/// Returns address lowercased, +/// with whitespace trimmed and `mailto:` prefix removed. +pub fn addr_normalize(addr: &str) -> String { + let norm = addr.trim().to_lowercase(); + + if norm.starts_with("mailto:") { + norm.get(7..).unwrap_or(&norm).to_string() + } else { + norm + } +} + +/// Compares two email addresses, normalizing them beforehand. +pub fn addr_cmp(addr1: &str, addr2: &str) -> bool { + let norm1 = addr_normalize(addr1); + let norm2 = addr_normalize(addr2); + + norm1 == norm2 +} + +/// +/// Represents an email address, right now just the `name@domain` portion. +/// +/// # Example +/// +/// ``` +/// use deltachat_contact_utils::EmailAddress; +/// let email = match EmailAddress::new("someone@example.com") { +/// Ok(addr) => addr, +/// Err(e) => panic!("Error parsing address, error was {}", e), +/// }; +/// assert_eq!(&email.local, "someone"); +/// assert_eq!(&email.domain, "example.com"); +/// assert_eq!(email.to_string(), "someone@example.com"); +/// ``` +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct EmailAddress { + /// Local part of the email address. + pub local: String, + + /// Email address domain. + pub domain: String, +} + +impl fmt::Display for EmailAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}@{}", self.local, self.domain) + } +} + +impl EmailAddress { + /// Performs a dead-simple parse of an email address. + pub fn new(input: &str) -> Result { + if input.is_empty() { + bail!("empty string is not valid"); + } + let parts: Vec<&str> = input.rsplitn(2, '@').collect(); + + if input + .chars() + .any(|c| c.is_whitespace() || c == '<' || c == '>') + { + bail!("Email {:?} must not contain whitespaces, '>' or '<'", input); + } + + match &parts[..] { + [domain, local] => { + if local.is_empty() { + bail!("empty string is not valid for local part in {:?}", input); + } + if domain.is_empty() { + bail!("missing domain after '@' in {:?}", input); + } + if domain.ends_with('.') { + bail!("Domain {domain:?} should not contain the dot in the end"); + } + Ok(EmailAddress { + local: (*local).to_string(), + domain: (*domain).to_string(), + }) + } + _ => bail!("Email {:?} must contain '@' character", input), + } + } +} + +impl rusqlite::types::ToSql for EmailAddress { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Text(self.to_string()); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_contact_address() -> Result<()> { + let alice_addr = "alice@example.org"; + let contact_address = ContactAddress::new(alice_addr)?; + assert_eq!(contact_address.as_ref(), alice_addr); + + let invalid_addr = "<> foobar"; + assert!(ContactAddress::new(invalid_addr).is_err()); + + Ok(()) + } + + #[test] + fn test_emailaddress_parse() { + assert_eq!(EmailAddress::new("").is_ok(), false); + assert_eq!( + EmailAddress::new("user@domain.tld").unwrap(), + EmailAddress { + local: "user".into(), + domain: "domain.tld".into(), + } + ); + assert_eq!( + EmailAddress::new("user@localhost").unwrap(), + EmailAddress { + local: "user".into(), + domain: "localhost".into() + } + ); + assert_eq!(EmailAddress::new("uuu").is_ok(), false); + assert_eq!(EmailAddress::new("dd.tt").is_ok(), false); + assert!(EmailAddress::new("tt.dd@uu").is_ok()); + assert!(EmailAddress::new("u@d").is_ok()); + assert!(EmailAddress::new("u@d.").is_err()); + assert!(EmailAddress::new("u@d.t").is_ok()); + assert_eq!( + EmailAddress::new("u@d.tt").unwrap(), + EmailAddress { + local: "u".into(), + domain: "d.tt".into(), + } + ); + assert!(EmailAddress::new("u@tt").is_ok()); + assert_eq!(EmailAddress::new("@d.tt").is_ok(), false); + } +} diff --git a/src/authres.rs b/src/authres.rs index ef8fe4fc86..d57b17c225 100644 --- a/src/authres.rs +++ b/src/authres.rs @@ -6,6 +6,7 @@ use std::collections::BTreeSet; use std::fmt; use anyhow::Result; +use deltachat_contact_utils::EmailAddress; use mailparse::MailHeaderMap; use mailparse::ParsedMail; use once_cell::sync::Lazy; @@ -14,7 +15,6 @@ use crate::config::Config; use crate::context::Context; use crate::headerdef::HeaderDef; use crate::tools::time; -use crate::tools::EmailAddress; /// `authres` is short for the Authentication-Results header, defined in /// , which contains info diff --git a/src/chat.rs b/src/chat.rs index 183f957141..83f992e4a1 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use std::time::Duration; use anyhow::{anyhow, bail, ensure, Context as _, Result}; +use deltachat_contact_utils::{strip_rtlo_characters, ContactAddress}; use deltachat_derive::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; @@ -21,7 +22,7 @@ use crate::constants::{ self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, }; -use crate::contact::{self, Contact, ContactAddress, ContactId, Origin}; +use crate::contact::{self, Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; use crate::download::DownloadState; @@ -43,7 +44,7 @@ use crate::sync::{self, Sync::*, SyncData}; use crate::tools::{ buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input, - smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty, SystemTime, + smeared_time, time, IsNoneOrEmpty, SystemTime, }; use crate::webxdc::WEBXDC_SUFFIX; diff --git a/src/config.rs b/src/config.rs index 84714bb30c..edd2256bff 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use anyhow::{ensure, Context as _, Result}; use base64::Engine as _; +use deltachat_contact_utils::addr_cmp; use serde::{Deserialize, Serialize}; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; @@ -13,7 +14,6 @@ use tokio::fs; use crate::blob::BlobObject; use crate::constants::{self, DC_VERSION_STR}; -use crate::contact::addr_cmp; use crate::context::Context; use crate::events::EventType; use crate::log::LogExt; diff --git a/src/configure.rs b/src/configure.rs index 60a44b2998..c57b0317b8 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -16,6 +16,7 @@ mod server_params; use anyhow::{bail, ensure, Context as _, Result}; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; +use deltachat_contact_utils::EmailAddress; use futures::FutureExt; use futures_lite::FutureExt as _; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; @@ -23,7 +24,6 @@ use server_params::{expand_param_vector, ServerParams}; use tokio::task; use crate::config::{self, Config}; -use crate::contact::addr_cmp; use crate::context::Context; use crate::imap::{session::Session as ImapSession, Imap}; use crate::log::LogExt; @@ -35,8 +35,9 @@ use crate::smtp::Smtp; use crate::socks::Socks5Config; use crate::stock_str; use crate::sync::Sync::*; -use crate::tools::{time, EmailAddress}; +use crate::tools::time; use crate::{chat, e2ee, provider}; +use deltachat_contact_utils::addr_cmp; macro_rules! progress { ($context:tt, $progress:expr, $comment:expr) => { diff --git a/src/contact.rs b/src/contact.rs index 2abcfab7d4..c40820c1ca 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -3,15 +3,17 @@ use std::cmp::Reverse; use std::collections::BinaryHeap; use std::fmt; -use std::ops::Deref; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; use anyhow::{bail, ensure, Context as _, Result}; use async_channel::{self as channel, Receiver, Sender}; +pub use deltachat_contact_utils::may_be_valid_addr; +use deltachat_contact_utils::{ + addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, strip_rtlo_characters, + ContactAddress, +}; use deltachat_derive::{FromSql, ToSql}; -use once_cell::sync::Lazy; -use regex::Regex; use rusqlite::OptionalExtension; use serde::{Deserialize, Serialize}; use tokio::task; @@ -33,60 +35,12 @@ use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::sql::{self, params_iter}; use crate::sync::{self, Sync::*}; -use crate::tools::{ - duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time, - EmailAddress, SystemTime, -}; +use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime}; use crate::{chat, stock_str}; /// Time during which a contact is considered as seen recently. const SEEN_RECENTLY_SECONDS: i64 = 600; -/// Valid contact address. -#[derive(Debug, Clone)] -pub(crate) struct ContactAddress(String); - -impl Deref for ContactAddress { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef for ContactAddress { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for ContactAddress { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl ContactAddress { - /// Constructs a new contact address from string, - /// normalizing and validating it. - pub fn new(s: &str) -> Result { - let addr = addr_normalize(s); - if !may_be_valid_addr(&addr) { - bail!("invalid address {:?}", s); - } - Ok(Self(addr.to_string())) - } -} - -/// Allow converting [`ContactAddress`] to an SQLite type. -impl rusqlite::types::ToSql for ContactAddress { - fn to_sql(&self) -> rusqlite::Result { - let val = rusqlite::types::Value::Text(self.0.to_string()); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - /// Contact ID, including reserved IDs. /// /// Some contact IDs are reserved to identify special contacts. This @@ -1412,46 +1366,6 @@ impl Contact { } } -/// Returns false if addr is an invalid address, otherwise true. -pub fn may_be_valid_addr(addr: &str) -> bool { - let res = EmailAddress::new(addr); - res.is_ok() -} - -/// Returns address lowercased, -/// with whitespace trimmed and `mailto:` prefix removed. -pub fn addr_normalize(addr: &str) -> String { - let norm = addr.trim().to_lowercase(); - - if norm.starts_with("mailto:") { - norm.get(7..).unwrap_or(&norm).to_string() - } else { - norm - } -} - -fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) { - static ADDR_WITH_NAME_REGEX: Lazy = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap()); - if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) { - ( - if name.is_empty() { - strip_rtlo_characters( - &captures - .get(1) - .map_or("".to_string(), |m| normalize_name(m.as_str())), - ) - } else { - strip_rtlo_characters(name) - }, - captures - .get(2) - .map_or("".to_string(), |m| m.as_str().to_string()), - ) - } else { - (strip_rtlo_characters(name), addr.to_string()) - } -} - pub(crate) async fn set_blocked( context: &Context, sync: sync::Sync, @@ -1638,26 +1552,6 @@ pub(crate) async fn update_last_seen( Ok(()) } -/// Normalize a name. -/// -/// - Remove quotes (come from some bad MUA implementations) -/// - Trims the resulting string -/// -/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`. -pub fn normalize_name(full_name: &str) -> String { - let full_name = full_name.trim(); - if full_name.is_empty() { - return full_name.into(); - } - - match full_name.as_bytes() { - [b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name - .get(1..full_name.len() - 1) - .map_or("".to_string(), |s| s.trim().to_string()), - _ => full_name.to_string(), - } -} - fn cat_fingerprint( ret: &mut String, addr: &str, @@ -1681,14 +1575,6 @@ fn cat_fingerprint( } } -/// Compares two email addresses, normalizing them beforehand. -pub fn addr_cmp(addr1: &str, addr2: &str) -> bool { - let norm1 = addr_normalize(addr1); - let norm2 = addr_normalize(addr2); - - norm1 == norm2 -} - fn split_address_book(book: &str) -> Vec<(&str, &str)> { book.lines() .collect::>() @@ -1851,6 +1737,8 @@ impl RecentlySeenLoop { #[cfg(test)] mod tests { + use deltachat_contact_utils::may_be_valid_addr; + use super::*; use crate::chat::{get_chat_contacts, send_text_msg, Chat}; use crate::chatlist::Chatlist; @@ -1990,18 +1878,6 @@ mod tests { Ok(()) } - #[test] - fn test_contact_address() -> Result<()> { - let alice_addr = "alice@example.org"; - let contact_address = ContactAddress::new(alice_addr)?; - assert_eq!(contact_address.as_ref(), alice_addr); - - let invalid_addr = "<> foobar"; - assert!(ContactAddress::new(invalid_addr).is_err()); - - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_or_lookup() { // add some contacts, this also tests add_address_book() diff --git a/src/decrypt.rs b/src/decrypt.rs index bfc2383118..38af64f552 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -4,12 +4,12 @@ use std::collections::HashSet; use std::str::FromStr; use anyhow::Result; +use deltachat_contact_utils::addr_cmp; use mailparse::ParsedMail; use crate::aheader::Aheader; use crate::authres::handle_authres; use crate::authres::{self, DkimResults}; -use crate::contact::addr_cmp; use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; diff --git a/src/imap.rs b/src/imap.rs index 24ea33e62c..2ef814a1e1 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -15,6 +15,7 @@ use std::{ use anyhow::{bail, format_err, Context as _, Result}; use async_channel::Receiver; use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse}; +use deltachat_contact_utils::{normalize_name, ContactAddress}; use futures::{FutureExt as _, StreamExt, TryStreamExt}; use futures_lite::FutureExt; use num_traits::FromPrimitive; @@ -24,7 +25,7 @@ use tokio::sync::RwLock; use crate::chat::{self, ChatId, ChatIdBlocked}; use crate::config::Config; use crate::constants::{self, Blocked, Chattype, ShowEmails}; -use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin}; +use crate::contact::{Contact, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; diff --git a/src/imex.rs b/src/imex.rs index 235c97f5ea..72dec921c9 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use ::pgp::types::KeyTrait; use anyhow::{bail, ensure, format_err, Context as _, Result}; +use deltachat_contact_utils::EmailAddress; use futures::StreamExt; use futures_lite::FutureExt; use rand::{thread_rng, Rng}; @@ -31,7 +32,6 @@ use crate::sql; use crate::stock_str; use crate::tools::{ create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file, - EmailAddress, }; mod transfer; diff --git a/src/key.rs b/src/key.rs index fb21d9fe05..22ab4660fb 100644 --- a/src/key.rs +++ b/src/key.rs @@ -6,6 +6,7 @@ use std::io::Cursor; use anyhow::{ensure, Context as _, Result}; use base64::Engine as _; +use deltachat_contact_utils::EmailAddress; use num_traits::FromPrimitive; use pgp::composed::Deserializable; pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; @@ -18,7 +19,7 @@ use crate::constants::KeyGenType; use crate::context::Context; use crate::log::LogExt; use crate::pgp::KeyPair; -use crate::tools::{self, time_elapsed, EmailAddress}; +use crate::tools::{self, time_elapsed}; /// Convenience trait for working with keys. /// diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 2ac1f02bcd..86a1043256 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1582,6 +1582,7 @@ fn maybe_encode_words(words: &str) -> String { #[cfg(test)] mod tests { + use deltachat_contact_utils::ContactAddress; use mailparse::{addrparse_header, MailHeaderMap}; use std::str; @@ -1593,7 +1594,7 @@ mod tests { }; use crate::chatlist::Chatlist; use crate::constants; - use crate::contact::{ContactAddress, Origin}; + use crate::contact::Origin; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf; use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 49c0982734..1149d7ebf7 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -6,6 +6,7 @@ use std::path::Path; use std::str; use anyhow::{bail, Context as _, Result}; +use deltachat_contact_utils::{addr_cmp, addr_normalize, strip_rtlo_characters}; use deltachat_derive::{FromSql, ToSql}; use format_flowed::unformat_flowed; use lettre_email::mime::Mime; @@ -16,7 +17,7 @@ use crate::blob::BlobObject; use crate::chat::{add_info_msg, ChatId}; use crate::config::Config; use crate::constants::{self, Chattype, DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; -use crate::contact::{addr_cmp, addr_normalize, Contact, ContactId, Origin}; +use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::decrypt::{ keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, @@ -35,8 +36,7 @@ use crate::simplify::{simplify, SimplifiedText}; use crate::stock_str; use crate::sync::SyncItems; use crate::tools::{ - create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, - strip_rtlo_characters, truncate_by_lines, + create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time, truncate_by_lines, }; use crate::{location, tools}; diff --git a/src/peerstate.rs b/src/peerstate.rs index 3d892e61c2..aa3cdb4f14 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -3,6 +3,7 @@ use std::mem; use anyhow::{Context as _, Error, Result}; +use deltachat_contact_utils::{addr_cmp, ContactAddress}; use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; @@ -10,7 +11,7 @@ use crate::chat::{self, Chat}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; -use crate::contact::{addr_cmp, Contact, ContactAddress, Origin}; +use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; diff --git a/src/pgp.rs b/src/pgp.rs index b5791a3403..811e969c0b 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -5,6 +5,7 @@ use std::io; use std::io::Cursor; use anyhow::{bail, Context as _, Result}; +use deltachat_contact_utils::EmailAddress; use pgp::armor::BlockType; use pgp::composed::{ Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey, @@ -20,7 +21,6 @@ use tokio::runtime::Handle; use crate::constants::KeyGenType; use crate::key::{DcKey, Fingerprint}; -use crate::tools::EmailAddress; #[allow(missing_docs)] #[cfg(test)] diff --git a/src/provider.rs b/src/provider.rs index 8de0da0038..0da679b18e 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -3,12 +3,12 @@ mod data; use anyhow::Result; +use deltachat_contact_utils::EmailAddress; use hickory_resolver::{config, AsyncResolver, TokioAsyncResolver}; use crate::config::Config; use crate::context::Context; use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS}; -use crate::tools::EmailAddress; /// Provider status according to manual testing. #[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)] diff --git a/src/qr.rs b/src/qr.rs index 7081b1d1bf..5e105f4516 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -5,6 +5,7 @@ use std::collections::BTreeMap; use anyhow::{anyhow, bail, ensure, Context as _, Result}; pub use dclogin_scheme::LoginOptions; +use deltachat_contact_utils::{addr_normalize, may_be_valid_addr, ContactAddress}; use once_cell::sync::Lazy; use percent_encoding::percent_decode_str; use serde::Deserialize; @@ -13,9 +14,7 @@ use self::dclogin_scheme::configure_from_login_qr; use crate::chat::{get_chat_id_by_grpid, ChatIdBlocked}; use crate::config::Config; use crate::constants::Blocked; -use crate::contact::{ - addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin, -}; +use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::events::EventType; use crate::key::Fingerprint; diff --git a/src/qr/dclogin_scheme.rs b/src/qr/dclogin_scheme.rs index 965e288c49..38a7033da6 100644 --- a/src/qr/dclogin_scheme.rs +++ b/src/qr/dclogin_scheme.rs @@ -1,13 +1,15 @@ use std::collections::HashMap; use anyhow::{bail, Context as _, Result}; + +use deltachat_contact_utils::may_be_valid_addr; use num_traits::cast::ToPrimitive; use super::{Qr, DCLOGIN_SCHEME}; use crate::config::Config; use crate::context::Context; +use crate::login_param::CertificateChecks; use crate::provider::Socket; -use crate::{contact, login_param::CertificateChecks}; /// Options for `dclogin:` scheme. #[derive(Debug, Clone, PartialEq, Eq)] @@ -88,7 +90,7 @@ pub(super) fn decode_login(qr: &str) -> Result { .collect(); // check if username is there - if !contact::may_be_valid_addr(addr) { + if !may_be_valid_addr(addr) { bail!("invalid DCLOGIN payload: invalid username E5"); } diff --git a/src/reaction.rs b/src/reaction.rs index e3e2ec8932..9741bc79f1 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -381,12 +381,14 @@ impl Chat { #[cfg(test)] mod tests { + use deltachat_contact_utils::ContactAddress; + use super::*; use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::DC_CHAT_ID_TRASH; - use crate::contact::{Contact, ContactAddress, Origin}; + use crate::contact::{Contact, Origin}; use crate::download::DownloadState; use crate::message::{delete_msgs, MessageState}; use crate::receive_imf::{receive_imf, receive_imf_from_inbox}; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ac1eb79039..437498f9d6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -3,6 +3,9 @@ use std::collections::HashSet; use anyhow::{Context as _, Result}; +use deltachat_contact_utils::{ + addr_cmp, may_be_valid_addr, normalize_name, strip_rtlo_characters, ContactAddress, +}; use mailparse::{parse_mail, SingleInfo}; use num_traits::FromPrimitive; use once_cell::sync::Lazy; @@ -12,9 +15,7 @@ use crate::aheader::EncryptPreference; use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; use crate::constants::{self, Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH}; -use crate::contact::{ - addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, -}; +use crate::contact::{Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc_inner; use crate::download::DownloadState; @@ -37,9 +38,7 @@ use crate::simplify; use crate::sql; use crate::stock_str; use crate::sync::Sync::*; -use crate::tools::{ - self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters, validate_id, -}; +use crate::tools::{self, buf_compress, extract_grpid_from_rfc724_mid, validate_id}; use crate::{contact, imap}; /// This is the struct that is returned after receiving one email (aka MIME message). diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 8b2dc895fd..3a5e9e799a 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -9,8 +9,8 @@ use tokio::sync::Mutex; use crate::events::EventType; use crate::imap::{scan_folders::get_watched_folder_configs, FolderMeaning}; use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE}; +use crate::stock_str; use crate::{context::Context, log::LogExt}; -use crate::{stock_str, tools}; use super::InnerSchedulerState; @@ -413,7 +413,9 @@ impl Context { // [======67%===== ] // ============================================================================================= - let domain = &tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain; + let domain = + &deltachat_contact_utils::EmailAddress::new(&self.get_primary_self_addr().await?)? + .domain; let storage_on_domain = stock_str::storage_on_domain(self, domain).await; ret += &format!("

{storage_on_domain}

    "); let quota = self.quota.read().await; diff --git a/src/securejoin.rs b/src/securejoin.rs index 0f1a7483d0..c14d5a0397 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -754,17 +754,18 @@ fn encrypted_and_signed( #[cfg(test)] mod tests { + use deltachat_contact_utils::{ContactAddress, EmailAddress}; + use super::*; use crate::chat::remove_contact_from_chat; use crate::chatlist::Chatlist; use crate::constants::Chattype; - use crate::contact::ContactAddress; use crate::imex::{imex, ImexMode}; use crate::receive_imf::receive_imf; use crate::stock_str::chat_protection_enabled; use crate::test_utils::get_chat_msg; use crate::test_utils::{TestContext, TestContextManager}; - use crate::tools::{EmailAddress, SystemTime}; + use crate::tools::SystemTime; use std::collections::HashSet; use std::time::Duration; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 8e96859572..85162d9048 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1,6 +1,7 @@ //! Migrations module. use anyhow::{Context as _, Result}; +use deltachat_contact_utils::EmailAddress; use rusqlite::OptionalExtension; use crate::config::Config; @@ -10,7 +11,6 @@ use crate::imap; use crate::message::MsgId; use crate::provider::get_provider_by_domain; use crate::sql::Sql; -use crate::tools::EmailAddress; const DBVERSION: i32 = 68; const VERSION_CFG: &str = "dbversion"; diff --git a/src/test_utils.rs b/src/test_utils.rs index af6888be24..5e158b0375 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -13,6 +13,7 @@ use std::time::{Duration, Instant}; use ansi_term::Color; use async_channel::{self as channel, Receiver, Sender}; use chat::ChatItem; +use deltachat_contact_utils::{ContactAddress, EmailAddress}; use once_cell::sync::Lazy; use pretty_assertions::assert_eq; use rand::Rng; @@ -29,7 +30,7 @@ use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::DC_GCL_NO_SPECIALS; use crate::constants::{Blocked, Chattype}; -use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin}; +use crate::contact::{Contact, ContactId, Modifier, Origin}; use crate::context::Context; use crate::e2ee::EncryptHelper; use crate::events::{Event, EventType, Events}; @@ -41,7 +42,6 @@ use crate::pgp::KeyPair; use crate::receive_imf::receive_imf; use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::stock_str::StockStrings; -use crate::tools::EmailAddress; #[allow(non_upper_case_globals)] pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png"); diff --git a/src/tools.rs b/src/tools.rs index aa434ca10d..27700a529f 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -4,7 +4,6 @@ #![allow(missing_docs)] use std::borrow::Cow; -use std::fmt; use std::io::{Cursor, Write}; use std::mem; use std::path::{Path, PathBuf}; @@ -23,6 +22,7 @@ pub use std::time::SystemTime; use anyhow::{bail, Context as _, Result}; use base64::Engine as _; use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone}; +use deltachat_contact_utils::{strip_rtlo_characters, EmailAddress}; #[cfg(test)] pub use deltachat_time::SystemTimeTools as SystemTime; use futures::{StreamExt, TryStreamExt}; @@ -536,80 +536,6 @@ pub fn parse_mailto(mailto_url: &str) -> Option { } } -/// -/// Represents an email address, right now just the `name@domain` portion. -/// -/// # Example -/// -/// ``` -/// use deltachat::tools::EmailAddress; -/// let email = match EmailAddress::new("someone@example.com") { -/// Ok(addr) => addr, -/// Err(e) => panic!("Error parsing address, error was {}", e), -/// }; -/// assert_eq!(&email.local, "someone"); -/// assert_eq!(&email.domain, "example.com"); -/// assert_eq!(email.to_string(), "someone@example.com"); -/// ``` -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct EmailAddress { - /// Local part of the email address. - pub local: String, - - /// Email address domain. - pub domain: String, -} - -impl fmt::Display for EmailAddress { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}@{}", self.local, self.domain) - } -} - -impl EmailAddress { - /// Performs a dead-simple parse of an email address. - pub fn new(input: &str) -> Result { - if input.is_empty() { - bail!("empty string is not valid"); - } - let parts: Vec<&str> = input.rsplitn(2, '@').collect(); - - if input - .chars() - .any(|c| c.is_whitespace() || c == '<' || c == '>') - { - bail!("Email {:?} must not contain whitespaces, '>' or '<'", input); - } - - match &parts[..] { - [domain, local] => { - if local.is_empty() { - bail!("empty string is not valid for local part in {:?}", input); - } - if domain.is_empty() { - bail!("missing domain after '@' in {:?}", input); - } - if domain.ends_with('.') { - bail!("Domain {domain:?} should not contain the dot in the end"); - } - Ok(EmailAddress { - local: (*local).to_string(), - domain: (*domain).to_string(), - }) - } - _ => bail!("Email {:?} must contain '@' character", input), - } - } -} - -impl rusqlite::types::ToSql for EmailAddress { - fn to_sql(&self) -> rusqlite::Result { - let val = rusqlite::types::Value::Text(self.to_string()); - let out = rusqlite::types::ToSqlOutput::Owned(val); - Ok(out) - } -} - /// Sanitizes user input /// - strip newlines /// - strip malicious bidi characters @@ -753,13 +679,6 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result> { Ok(mem::take(decompressor.get_mut())) } -const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}']; -/// This method strips all occurrences of the RTLO Unicode character. -/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)? -pub(crate) fn strip_rtlo_characters(input_str: &str) -> String { - input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "") -} - #[cfg(test)] mod tests { #![allow(clippy::indexing_slicing)] @@ -1042,40 +961,6 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; assert!(extract_grpid_from_rfc724_mid(mid.as_str()).is_none()); } - #[test] - fn test_emailaddress_parse() { - assert_eq!(EmailAddress::new("").is_ok(), false); - assert_eq!( - EmailAddress::new("user@domain.tld").unwrap(), - EmailAddress { - local: "user".into(), - domain: "domain.tld".into(), - } - ); - assert_eq!( - EmailAddress::new("user@localhost").unwrap(), - EmailAddress { - local: "user".into(), - domain: "localhost".into() - } - ); - assert_eq!(EmailAddress::new("uuu").is_ok(), false); - assert_eq!(EmailAddress::new("dd.tt").is_ok(), false); - assert!(EmailAddress::new("tt.dd@uu").is_ok()); - assert!(EmailAddress::new("u@d").is_ok()); - assert!(EmailAddress::new("u@d.").is_err()); - assert!(EmailAddress::new("u@d.t").is_ok()); - assert_eq!( - EmailAddress::new("u@d.tt").unwrap(), - EmailAddress { - local: "u".into(), - domain: "d.tt".into(), - } - ); - assert!(EmailAddress::new("u@tt").is_ok()); - assert_eq!(EmailAddress::new("@d.tt").is_ok(), false); - } - use chrono::NaiveDate; use proptest::prelude::*; diff --git a/src/webxdc.rs b/src/webxdc.rs index fc5f36b57e..1f3eb534b4 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -19,6 +19,7 @@ use std::path::Path; use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result}; +use deltachat_contact_utils::strip_rtlo_characters; use deltachat_derive::FromSql; use lettre_email::PartBuilder; use rusqlite::OptionalExtension; @@ -38,7 +39,6 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::param::Params; use crate::tools::create_id; -use crate::tools::strip_rtlo_characters; use crate::tools::{create_smeared_timestamp, get_abs_path}; /// The current API version.