diff --git a/src/config.rs b/src/config.rs index a25110860..57a5ce415 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,11 +17,11 @@ use crate::configure::EnteredLoginParam; use crate::context::Context; use crate::events::EventType; use crate::log::{LogExt, info}; -use crate::login_param::ConfiguredLoginParam; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::provider::{Provider, get_provider_by_id}; use crate::sync::{self, Sync::*, SyncData}; use crate::tools::get_abs_path; +use crate::transport::ConfiguredLoginParam; use crate::{constants, stats}; /// The available configuration keys. diff --git a/src/configure.rs b/src/configure.rs index fafe5bfa8..5db0f07b5 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -28,18 +28,20 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT; use crate::context::Context; use crate::imap::Imap; use crate::log::{LogExt, info, warn}; +use crate::login_param::EnteredCertificateChecks; pub use crate::login_param::EnteredLoginParam; -use crate::login_param::{ - ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, - ConnectionCandidate, EnteredCertificateChecks, ProxyConfig, -}; use crate::message::Message; +use crate::net::proxy::ProxyConfig; use crate::oauth2::get_oauth2_addr; use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; use crate::qr::{login_param_from_account_qr, login_param_from_login_qr}; use crate::smtp::Smtp; use crate::sync::Sync::*; use crate::tools::time; +use crate::transport::{ + ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, + ConnectionCandidate, +}; use crate::{EventType, stock_str}; use crate::{chat, provider}; use deltachat_contact_tools::addr_cmp; diff --git a/src/context.rs b/src/context.rs index 4ced437f0..570d5acf4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -23,7 +23,7 @@ use crate::imap::{FolderMeaning, Imap, ServerMetadata}; use crate::key::self_fingerprint; use crate::log::{info, warn}; use crate::logged_debug_assert; -use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; +use crate::login_param::EnteredLoginParam; use crate::message::{self, MessageState, MsgId}; use crate::net::tls::TlsSessionStore; use crate::peer_channels::Iroh; @@ -34,6 +34,7 @@ use crate::sql::Sql; use crate::stock_str::StockStrings; use crate::timesmearing::SmearedTimestamp; use crate::tools::{self, duration_to_str, time, time_elapsed}; +use crate::transport::ConfiguredLoginParam; use crate::{chatlist_events, stats}; /// Builder for the [`Context`]. diff --git a/src/imap.rs b/src/imap.rs index a71f264f1..1555ac0af 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -33,9 +33,6 @@ use crate::context::Context; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::log::{LogExt, error, info, warn}; -use crate::login_param::{ - ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params, -}; use crate::message::{self, Message, MessageState, MessengerMessage, MsgId}; use crate::mimeparser; use crate::net::proxy::ProxyConfig; @@ -48,6 +45,9 @@ use crate::receive_imf::{ use crate::scheduler::connectivity::ConnectivityStore; use crate::stock_str; use crate::tools::{self, create_id, duration_to_str, time}; +use crate::transport::{ + ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params, +}; pub(crate) mod capabilities; mod client; diff --git a/src/imap/client.rs b/src/imap/client.rs index bd5b13659..81c0ce08e 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -9,13 +9,14 @@ use tokio::io::BufWriter; use super::capabilities::Capabilities; use crate::context::Context; use crate::log::{LoggingStream, info, warn}; -use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionStream; use crate::net::tls::wrap_tls; use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history}; use crate::tools::time; +use crate::transport::ConnectionCandidate; +use crate::transport::ConnectionSecurity; #[derive(Debug)] pub(crate) struct Client { diff --git a/src/lib.rs b/src/lib.rs index 089b48686..341f03d69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub mod stock_str; mod sync; mod timesmearing; mod token; +mod transport; mod update_helper; pub mod webxdc; #[macro_use] diff --git a/src/login_param.rs b/src/login_param.rs index 5356a11a0..5c6864f11 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -1,21 +1,22 @@ //! # Login parameters. +//! +//! Login parameters are entered by the user +//! to configure a new transport. +//! Login parameters may also be entered +//! implicitly by scanning a QR code +//! of `dcaccount:` or `dclogin:` scheme. use std::fmt; -use anyhow::{Context as _, Result, bail, ensure, format_err}; -use deltachat_contact_tools::{EmailAddress, addr_cmp, addr_normalize}; +use anyhow::{Context as _, Result}; use num_traits::ToPrimitive as _; use serde::{Deserialize, Serialize}; use crate::config::Config; -use crate::configure::server_params::{ServerParams, expand_param_vector}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2}; use crate::context::Context; -use crate::net::load_connection_timestamp; pub use crate::net::proxy::ProxyConfig; pub use crate::provider::Socket; -use crate::provider::{Protocol, Provider, UsernamePattern, get_provider_by_id}; -use crate::sql::Sql; use crate::tools::ToOption; /// User-entered setting for certificate checks. @@ -55,45 +56,6 @@ pub enum EnteredCertificateChecks { AcceptInvalidCertificates2 = 3, } -/// Values saved into `imap_certificate_checks`. -#[derive( - Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize, -)] -#[repr(u32)] -#[strum(serialize_all = "snake_case")] -pub(crate) enum ConfiguredCertificateChecks { - /// Use configuration from the provider database. - /// If there is no provider database setting for certificate checks, - /// accept invalid certificates. - /// - /// Must not be saved by new versions. - /// - /// Previous Delta Chat versions before core 1.133.0 - /// stored this in `configured_imap_certificate_checks` - /// if Automatic configuration - /// was selected, configuration with strict TLS checks failed - /// and configuration without strict TLS checks succeeded. - OldAutomatic = 0, - - /// Ensure that TLS certificate is valid for the server hostname. - Strict = 1, - - /// Accept certificates that are expired, self-signed - /// or otherwise not valid for the server hostname. - AcceptInvalidCertificates = 2, - - /// Accept certificates that are expired, self-signed - /// or otherwise not valid for the server hostname. - /// - /// Alias to `AcceptInvalidCertificates` for compatibility. - AcceptInvalidCertificates2 = 3, - - /// Use configuration from the provider database. - /// If there is no provider database setting for certificate checks, - /// apply strict checks to TLS certificates. - Automatic = 4, -} - /// Login parameters for a single server, either IMAP or SMTP #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EnteredServerLoginParam { @@ -333,584 +295,20 @@ fn unset_empty(s: &str) -> &str { if s.is_empty() { "unset" } else { s } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct ConnectionCandidate { - /// Server hostname or IP address. - pub host: String, - - /// Server port. - pub port: u16, - - /// Transport layer security. - pub security: ConnectionSecurity, -} - -impl fmt::Display for ConnectionCandidate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}:{}", &self.host, self.port, self.security)?; - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) enum ConnectionSecurity { - /// Implicit TLS. - Tls, - - // STARTTLS. - Starttls, - - /// Plaintext. - Plain, -} - -impl fmt::Display for ConnectionSecurity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Tls => write!(f, "tls")?, - Self::Starttls => write!(f, "starttls")?, - Self::Plain => write!(f, "plain")?, - } - Ok(()) - } -} - -impl TryFrom for ConnectionSecurity { - type Error = anyhow::Error; - - fn try_from(socket: Socket) -> Result { - match socket { - Socket::Automatic => Err(format_err!("Socket security is not configured")), - Socket::Ssl => Ok(Self::Tls), - Socket::Starttls => Ok(Self::Starttls), - Socket::Plain => Ok(Self::Plain), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct ConfiguredServerLoginParam { - pub connection: ConnectionCandidate, - - /// Username. - pub user: String, -} - -impl fmt::Display for ConfiguredServerLoginParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.connection, &self.user)?; - Ok(()) - } -} - -pub(crate) async fn prioritize_server_login_params( - sql: &Sql, - params: &[ConfiguredServerLoginParam], - alpn: &str, -) -> Result> { - let mut res: Vec<(Option, ConfiguredServerLoginParam)> = Vec::with_capacity(params.len()); - for param in params { - let timestamp = load_connection_timestamp( - sql, - alpn, - ¶m.connection.host, - param.connection.port, - None, - ) - .await?; - res.push((timestamp, param.clone())); - } - res.sort_by_key(|(ts, _param)| std::cmp::Reverse(*ts)); - Ok(res.into_iter().map(|(_ts, param)| param).collect()) -} - -/// Login parameters saved to the database -/// after successful configuration. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct ConfiguredLoginParam { - /// `From:` address that was used at the time of configuration. - pub addr: String, - - pub imap: Vec, - - // Custom IMAP user. - // - // This overwrites autoconfig from the provider database - // if non-empty. - pub imap_user: String, - - pub imap_password: String, - - pub smtp: Vec, - - // Custom SMTP user. - // - // This overwrites autoconfig from the provider database - // if non-empty. - pub smtp_user: String, - - pub smtp_password: String, - - pub provider: Option<&'static Provider>, - - /// TLS options: whether to allow invalid certificates and/or - /// invalid hostnames - pub certificate_checks: ConfiguredCertificateChecks, - - /// If true, login via OAUTH2 (not recommended anymore) - pub oauth2: bool, -} - -/// The representation of ConfiguredLoginParam in the database, -/// saved as Json. -#[derive(Debug, Serialize, Deserialize)] -struct ConfiguredLoginParamJson { - pub addr: String, - pub imap: Vec, - pub imap_user: String, - pub imap_password: String, - pub smtp: Vec, - pub smtp_user: String, - pub smtp_password: String, - pub provider_id: Option, - pub certificate_checks: ConfiguredCertificateChecks, - pub oauth2: bool, -} - -impl fmt::Display for ConfiguredLoginParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let addr = &self.addr; - let provider_id = match self.provider { - Some(provider) => provider.id, - None => "none", - }; - let certificate_checks = self.certificate_checks; - write!(f, "{addr} imap:[")?; - let mut first = true; - for imap in &self.imap { - if !first { - write!(f, ", ")?; - } - write!(f, "{imap}")?; - first = false; - } - write!(f, "] smtp:[")?; - let mut first = true; - for smtp in &self.smtp { - if !first { - write!(f, ", ")?; - } - write!(f, "{smtp}")?; - first = false; - } - write!(f, "] provider:{provider_id} cert_{certificate_checks}")?; - Ok(()) - } -} - -impl ConfiguredLoginParam { - /// Load configured account settings from the database. - /// - /// Returns `None` if account is not configured. - pub(crate) async fn load(context: &Context) -> Result> { - let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else { - return Ok(None); - }; - - let json: Option = context - .sql - .query_get_value( - "SELECT configured_param FROM transports WHERE addr=?", - (&self_addr,), - ) - .await?; - if let Some(json) = json { - Ok(Some(Self::from_json(&json)?)) - } else { - bail!("Self address {self_addr} doesn't have a corresponding transport"); - } - } - - /// Loads legacy configured param. Only used for tests and the migration. - pub(crate) async fn load_legacy(context: &Context) -> Result> { - if !context.get_config_bool(Config::Configured).await? { - return Ok(None); - } - - let addr = context - .get_config(Config::ConfiguredAddr) - .await? - .unwrap_or_default() - .trim() - .to_string(); - - let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) = - context - .get_config_parsed::(Config::ConfiguredImapCertificateChecks) - .await? - { - num_traits::FromPrimitive::from_i32(certificate_checks) - .context("Invalid configured_imap_certificate_checks value")? - } else { - // This is true for old accounts configured using C core - // which did not check TLS certificates. - ConfiguredCertificateChecks::OldAutomatic - }; - - let send_pw = context - .get_config(Config::ConfiguredSendPw) - .await? - .context("SMTP password is not configured")?; - let mail_pw = context - .get_config(Config::ConfiguredMailPw) - .await? - .context("IMAP password is not configured")?; - - let server_flags = context - .get_config_parsed::(Config::ConfiguredServerFlags) - .await? - .unwrap_or_default(); - let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); - - let provider = context.get_configured_provider().await?; - - let imap; - let smtp; - - let mail_user = context - .get_config(Config::ConfiguredMailUser) - .await? - .unwrap_or_default(); - let send_user = context - .get_config(Config::ConfiguredSendUser) - .await? - .unwrap_or_default(); - - if let Some(provider) = provider { - let parsed_addr = EmailAddress::new(&addr).context("Bad email-address")?; - let addr_localpart = parsed_addr.local; - - if provider.server.is_empty() { - let servers = vec![ - ServerParams { - protocol: Protocol::Imap, - hostname: context - .get_config(Config::ConfiguredMailServer) - .await? - .unwrap_or_default(), - port: context - .get_config_parsed::(Config::ConfiguredMailPort) - .await? - .unwrap_or_default(), - socket: context - .get_config_parsed::(Config::ConfiguredMailSecurity) - .await? - .and_then(num_traits::FromPrimitive::from_i32) - .unwrap_or_default(), - username: mail_user.clone(), - }, - ServerParams { - protocol: Protocol::Smtp, - hostname: context - .get_config(Config::ConfiguredSendServer) - .await? - .unwrap_or_default(), - port: context - .get_config_parsed::(Config::ConfiguredSendPort) - .await? - .unwrap_or_default(), - socket: context - .get_config_parsed::(Config::ConfiguredSendSecurity) - .await? - .and_then(num_traits::FromPrimitive::from_i32) - .unwrap_or_default(), - username: send_user.clone(), - }, - ]; - let servers = expand_param_vector(servers, &addr, &parsed_addr.domain); - imap = servers - .iter() - .filter_map(|params| { - let Ok(security) = params.socket.try_into() else { - return None; - }; - if params.protocol == Protocol::Imap { - Some(ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: params.hostname.clone(), - port: params.port, - security, - }, - user: params.username.clone(), - }) - } else { - None - } - }) - .collect(); - smtp = servers - .iter() - .filter_map(|params| { - let Ok(security) = params.socket.try_into() else { - return None; - }; - if params.protocol == Protocol::Smtp { - Some(ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: params.hostname.clone(), - port: params.port, - security, - }, - user: params.username.clone(), - }) - } else { - None - } - }) - .collect(); - } else { - imap = provider - .server - .iter() - .filter_map(|server| { - if server.protocol != Protocol::Imap { - return None; - } - - let Ok(security) = server.socket.try_into() else { - return None; - }; - - Some(ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: server.hostname.to_string(), - port: server.port, - security, - }, - user: if !mail_user.is_empty() { - mail_user.clone() - } else { - match server.username_pattern { - UsernamePattern::Email => addr.to_string(), - UsernamePattern::Emaillocalpart => addr_localpart.clone(), - } - }, - }) - }) - .collect(); - smtp = provider - .server - .iter() - .filter_map(|server| { - if server.protocol != Protocol::Smtp { - return None; - } - - let Ok(security) = server.socket.try_into() else { - return None; - }; - - Some(ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: server.hostname.to_string(), - port: server.port, - security, - }, - user: if !send_user.is_empty() { - send_user.clone() - } else { - match server.username_pattern { - UsernamePattern::Email => addr.to_string(), - UsernamePattern::Emaillocalpart => addr_localpart.clone(), - } - }, - }) - }) - .collect(); - } - } else if let (Some(configured_mail_servers), Some(configured_send_servers)) = ( - context.get_config(Config::ConfiguredImapServers).await?, - context.get_config(Config::ConfiguredSmtpServers).await?, - ) { - imap = serde_json::from_str(&configured_mail_servers) - .context("Failed to parse configured IMAP servers")?; - smtp = serde_json::from_str(&configured_send_servers) - .context("Failed to parse configured SMTP servers")?; - } else { - // Load legacy settings storing a single IMAP and single SMTP server. - let mail_server = context - .get_config(Config::ConfiguredMailServer) - .await? - .unwrap_or_default(); - let mail_port = context - .get_config_parsed::(Config::ConfiguredMailPort) - .await? - .unwrap_or_default(); - - let mail_security: Socket = context - .get_config_parsed::(Config::ConfiguredMailSecurity) - .await? - .and_then(num_traits::FromPrimitive::from_i32) - .unwrap_or_default(); - - let send_server = context - .get_config(Config::ConfiguredSendServer) - .await? - .context("SMTP server is not configured")?; - let send_port = context - .get_config_parsed::(Config::ConfiguredSendPort) - .await? - .unwrap_or_default(); - let send_security: Socket = context - .get_config_parsed::(Config::ConfiguredSendSecurity) - .await? - .and_then(num_traits::FromPrimitive::from_i32) - .unwrap_or_default(); - - imap = vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: mail_server, - port: mail_port, - security: mail_security.try_into()?, - }, - user: mail_user.clone(), - }]; - smtp = vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: send_server, - port: send_port, - security: send_security.try_into()?, - }, - user: send_user.clone(), - }]; - } - - Ok(Some(ConfiguredLoginParam { - addr, - imap, - imap_user: mail_user, - imap_password: mail_pw, - smtp, - smtp_user: send_user, - smtp_password: send_pw, - certificate_checks, - provider, - oauth2, - })) - } - - pub(crate) async fn save_to_transports_table( - self, - context: &Context, - entered_param: &EnteredLoginParam, - ) -> Result<()> { - let addr = addr_normalize(&self.addr); - let provider_id = self.provider.map(|provider| provider.id); - let configured_addr = context.get_config(Config::ConfiguredAddr).await?; - if let Some(configured_addr) = &configured_addr { - ensure!( - addr_cmp(configured_addr, &addr), - "Adding a second transport is not supported right now." - ); - } - context - .sql - .execute( - "INSERT INTO transports (addr, entered_param, configured_param) - VALUES (?, ?, ?) - ON CONFLICT (addr) - DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param", - ( - self.addr.clone(), - serde_json::to_string(entered_param)?, - self.into_json()?, - ), - ) - .await?; - if configured_addr.is_none() { - // If there is no transport yet, set the new transport as the primary one - context - .sql - .set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id) - .await?; - context - .sql - .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) - .await?; - } - Ok(()) - } - - pub(crate) fn from_json(json: &str) -> Result { - let json: ConfiguredLoginParamJson = serde_json::from_str(json)?; - - let provider = json.provider_id.and_then(|id| get_provider_by_id(&id)); - - Ok(ConfiguredLoginParam { - addr: json.addr, - imap: json.imap, - imap_user: json.imap_user, - imap_password: json.imap_password, - smtp: json.smtp, - smtp_user: json.smtp_user, - smtp_password: json.smtp_password, - provider, - certificate_checks: json.certificate_checks, - oauth2: json.oauth2, - }) - } - - pub(crate) fn into_json(self) -> Result { - let json = ConfiguredLoginParamJson { - addr: self.addr, - imap: self.imap, - imap_user: self.imap_user, - imap_password: self.imap_password, - smtp: self.smtp, - smtp_user: self.smtp_user, - smtp_password: self.smtp_password, - provider_id: self.provider.map(|p| p.id.to_string()), - certificate_checks: self.certificate_checks, - oauth2: self.oauth2, - }; - Ok(serde_json::to_string(&json)?) - } - - pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool { - let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); - match self.certificate_checks { - ConfiguredCertificateChecks::OldAutomatic => { - provider_strict_tls.unwrap_or(connected_through_proxy) - } - ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true), - ConfiguredCertificateChecks::Strict => true, - ConfiguredCertificateChecks::AcceptInvalidCertificates - | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false, - } - } -} - #[cfg(test)] mod tests { use super::*; - use crate::log::LogExt as _; - use crate::provider::get_provider_by_id; use crate::test_utils::TestContext; use pretty_assertions::assert_eq; #[test] - fn test_certificate_checks_display() { + fn test_entered_certificate_checks_display() { use std::string::ToString; assert_eq!( "accept_invalid_certificates".to_string(), EnteredCertificateChecks::AcceptInvalidCertificates.to_string() ); - - assert_eq!( - "accept_invalid_certificates".to_string(), - ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string() - ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -976,267 +374,4 @@ mod tests { Ok(()) } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_load_login_param() -> Result<()> { - let t = TestContext::new().await; - - let param = ConfiguredLoginParam { - addr: "alice@example.org".to_string(), - imap: vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "imap.example.com".to_string(), - port: 123, - security: ConnectionSecurity::Starttls, - }, - user: "alice".to_string(), - }], - imap_user: "".to_string(), - imap_password: "foo".to_string(), - smtp: vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "smtp.example.com".to_string(), - port: 456, - security: ConnectionSecurity::Tls, - }, - user: "alice@example.org".to_string(), - }], - smtp_user: "".to_string(), - smtp_password: "bar".to_string(), - provider: None, - certificate_checks: ConfiguredCertificateChecks::Strict, - oauth2: false, - }; - - param - .clone() - .save_to_transports_table(&t, &EnteredLoginParam::default()) - .await?; - let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#; - assert_eq!( - t.sql - .query_get_value::("SELECT configured_param FROM transports", ()) - .await? - .unwrap(), - expected_param - ); - assert_eq!(t.is_configured().await?, true); - let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); - assert_eq!(param, loaded); - - // Legacy ConfiguredImapCertificateChecks config is ignored - t.set_config(Config::ConfiguredImapCertificateChecks, Some("999")) - .await?; - assert!(ConfiguredLoginParam::load(&t).await.is_ok()); - - // Test that we don't panic on unknown ConfiguredImapCertificateChecks values. - let wrong_param = expected_param.replace("Strict", "Stricct"); - assert_ne!(expected_param, wrong_param); - t.sql - .execute("UPDATE transports SET configured_param=?", (wrong_param,)) - .await?; - assert!(ConfiguredLoginParam::load(&t).await.is_err()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_posteo_alias() -> Result<()> { - let t = TestContext::new().await; - - let user = "alice@posteo.de"; - - // Alice has old config with "alice@posteo.at" address - // and "alice@posteo.de" username. - t.set_config(Config::Configured, Some("1")).await?; - t.set_config(Config::ConfiguredProvider, Some("posteo")) - .await?; - t.sql - .set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at")) - .await?; - t.set_config(Config::ConfiguredMailServer, Some("posteo.de")) - .await?; - t.set_config(Config::ConfiguredMailPort, Some("993")) - .await?; - t.set_config(Config::ConfiguredMailSecurity, Some("1")) - .await?; // TLS - t.set_config(Config::ConfiguredMailUser, Some(user)).await?; - t.set_config(Config::ConfiguredMailPw, Some("foobarbaz")) - .await?; - t.set_config(Config::ConfiguredImapCertificateChecks, Some("1")) - .await?; // Strict - t.set_config(Config::ConfiguredSendServer, Some("posteo.de")) - .await?; - t.set_config(Config::ConfiguredSendPort, Some("465")) - .await?; - t.set_config(Config::ConfiguredSendSecurity, Some("1")) - .await?; // TLS - t.set_config(Config::ConfiguredSendUser, Some(user)).await?; - t.set_config(Config::ConfiguredSendPw, Some("foobarbaz")) - .await?; - t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1")) - .await?; // Strict - t.set_config(Config::ConfiguredServerFlags, Some("0")) - .await?; - - let param = ConfiguredLoginParam { - addr: "alice@posteo.at".to_string(), - imap: vec![ - ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "posteo.de".to_string(), - port: 993, - security: ConnectionSecurity::Tls, - }, - user: user.to_string(), - }, - ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "posteo.de".to_string(), - port: 143, - security: ConnectionSecurity::Starttls, - }, - user: user.to_string(), - }, - ], - imap_user: "alice@posteo.de".to_string(), - imap_password: "foobarbaz".to_string(), - smtp: vec![ - ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "posteo.de".to_string(), - port: 465, - security: ConnectionSecurity::Tls, - }, - user: user.to_string(), - }, - ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "posteo.de".to_string(), - port: 587, - security: ConnectionSecurity::Starttls, - }, - user: user.to_string(), - }, - ], - smtp_user: "alice@posteo.de".to_string(), - smtp_password: "foobarbaz".to_string(), - provider: get_provider_by_id("posteo"), - certificate_checks: ConfiguredCertificateChecks::Strict, - oauth2: false, - }; - - let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); - assert_eq!(loaded, param); - - migrate_configured_login_param(&t).await; - let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); - assert_eq!(loaded, param); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_empty_server_list_legacy() -> Result<()> { - // Find a provider that does not have server list set. - // - // There is at least one such provider in the provider database. - let (domain, provider) = crate::provider::data::PROVIDER_DATA - .iter() - .find(|(_domain, provider)| provider.server.is_empty()) - .unwrap(); - - let t = TestContext::new().await; - - let addr = format!("alice@{domain}"); - - t.set_config(Config::Configured, Some("1")).await?; - t.set_config(Config::ConfiguredProvider, Some(provider.id)) - .await?; - t.sql - .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) - .await?; - t.set_config(Config::ConfiguredMailPw, Some("foobarbaz")) - .await?; - t.set_config(Config::ConfiguredImapCertificateChecks, Some("1")) - .await?; // Strict - t.set_config(Config::ConfiguredSendPw, Some("foobarbaz")) - .await?; - t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1")) - .await?; // Strict - t.set_config(Config::ConfiguredServerFlags, Some("0")) - .await?; - - let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); - assert_eq!(loaded.provider, Some(*provider)); - assert_eq!(loaded.imap.is_empty(), false); - assert_eq!(loaded.smtp.is_empty(), false); - - migrate_configured_login_param(&t).await; - - let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); - assert_eq!(loaded.provider, Some(*provider)); - assert_eq!(loaded.imap.is_empty(), false); - assert_eq!(loaded.smtp.is_empty(), false); - - Ok(()) - } - - async fn migrate_configured_login_param(t: &TestContext) { - t.sql.execute("DROP TABLE transports;", ()).await.unwrap(); - t.sql.set_raw_config_int("dbversion", 130).await.unwrap(); - t.sql.run_migrations(t).await.log_err(t).ok(); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_empty_server_list() -> Result<()> { - // Find a provider that does not have server list set. - // - // There is at least one such provider in the provider database. - let (domain, provider) = crate::provider::data::PROVIDER_DATA - .iter() - .find(|(_domain, provider)| provider.server.is_empty()) - .unwrap(); - - let t = TestContext::new().await; - - let addr = format!("alice@{domain}"); - - ConfiguredLoginParam { - addr: addr.clone(), - imap: vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "example.org".to_string(), - port: 100, - security: ConnectionSecurity::Tls, - }, - user: addr.clone(), - }], - imap_user: addr.clone(), - imap_password: "foobarbaz".to_string(), - smtp: vec![ConfiguredServerLoginParam { - connection: ConnectionCandidate { - host: "example.org".to_string(), - port: 100, - security: ConnectionSecurity::Tls, - }, - user: addr.clone(), - }], - smtp_user: addr.clone(), - smtp_password: "foobarbaz".to_string(), - provider: Some(provider), - certificate_checks: ConfiguredCertificateChecks::Automatic, - oauth2: false, - } - .save_to_transports_table(&t, &EnteredLoginParam::default()) - .await?; - - let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); - assert_eq!(loaded.provider, Some(*provider)); - assert_eq!(loaded.imap.is_empty(), false); - assert_eq!(loaded.smtp.is_empty(), false); - assert_eq!(t.get_configured_provider().await?, Some(*provider)); - - Ok(()) - } } diff --git a/src/smtp.rs b/src/smtp.rs index 58c7af058..f19637060 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -14,8 +14,6 @@ use crate::contact::{Contact, ContactId}; use crate::context::Context; use crate::events::EventType; use crate::log::{error, info, warn}; -use crate::login_param::prioritize_server_login_params; -use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam}; use crate::message::Message; use crate::message::{self, MsgId}; use crate::mimefactory::MimeFactory; @@ -24,6 +22,9 @@ use crate::net::session::SessionBufStream; use crate::scheduler::connectivity::ConnectivityStore; use crate::stock_str::unencrypted_email; use crate::tools::{self, time_elapsed}; +use crate::transport::{ + ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params, +}; #[derive(Default)] pub(crate) struct Smtp { diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index f7b35d657..efdeaa7b6 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -8,7 +8,6 @@ use tokio::io::{AsyncBufRead, AsyncWrite, BufStream}; use crate::context::Context; use crate::log::{info, warn}; -use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; @@ -18,6 +17,8 @@ use crate::net::{ }; use crate::oauth2::get_oauth2_access_token; use crate::tools::time; +use crate::transport::ConnectionCandidate; +use crate::transport::ConnectionSecurity; /// Converts port number to ALPN. fn alpn(port: u16) -> &'static str { diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index ac5400944..224dccfcb 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -17,11 +17,11 @@ use crate::context::Context; use crate::imap; use crate::key::DcKey; use crate::log::{info, warn}; -use crate::login_param::ConfiguredLoginParam; use crate::message::MsgId; use crate::provider::get_provider_info; use crate::sql::Sql; use crate::tools::{Time, inc_and_check, time_elapsed}; +use crate::transport::ConfiguredLoginParam; const DBVERSION: i32 = 68; const VERSION_CFG: &str = "dbversion"; diff --git a/src/transport.rs b/src/transport.rs new file mode 100644 index 000000000..88bfbcb24 --- /dev/null +++ b/src/transport.rs @@ -0,0 +1,901 @@ +//! # Message transport. +//! +//! A transport represents a single IMAP+SMTP configuration +//! that is known to work at least once in the past. +//! +//! Transports are stored in the `transports` SQL table. +//! Each transport is uniquely identified by its email address. +//! The table stores both the login parameters entered by the user +//! and configured list of connection candidates. + +use std::fmt; + +use anyhow::{Context as _, Result, bail, ensure, format_err}; +use deltachat_contact_tools::{EmailAddress, addr_cmp, addr_normalize}; +use serde::{Deserialize, Serialize}; + +use crate::config::Config; +use crate::configure::server_params::{ServerParams, expand_param_vector}; +use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2}; +use crate::context::Context; +use crate::login_param::EnteredLoginParam; +use crate::net::load_connection_timestamp; +use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id}; +use crate::sql::Sql; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum ConnectionSecurity { + /// Implicit TLS. + Tls, + + // STARTTLS. + Starttls, + + /// Plaintext. + Plain, +} + +impl fmt::Display for ConnectionSecurity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Tls => write!(f, "tls")?, + Self::Starttls => write!(f, "starttls")?, + Self::Plain => write!(f, "plain")?, + } + Ok(()) + } +} + +impl TryFrom for ConnectionSecurity { + type Error = anyhow::Error; + + fn try_from(socket: Socket) -> Result { + match socket { + Socket::Automatic => Err(format_err!("Socket security is not configured")), + Socket::Ssl => Ok(Self::Tls), + Socket::Starttls => Ok(Self::Starttls), + Socket::Plain => Ok(Self::Plain), + } + } +} + +/// Values saved into `imap_certificate_checks`. +#[derive( + Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize, +)] +#[repr(u32)] +#[strum(serialize_all = "snake_case")] +pub(crate) enum ConfiguredCertificateChecks { + /// Use configuration from the provider database. + /// If there is no provider database setting for certificate checks, + /// accept invalid certificates. + /// + /// Must not be saved by new versions. + /// + /// Previous Delta Chat versions before core 1.133.0 + /// stored this in `configured_imap_certificate_checks` + /// if Automatic configuration + /// was selected, configuration with strict TLS checks failed + /// and configuration without strict TLS checks succeeded. + OldAutomatic = 0, + + /// Ensure that TLS certificate is valid for the server hostname. + Strict = 1, + + /// Accept certificates that are expired, self-signed + /// or otherwise not valid for the server hostname. + AcceptInvalidCertificates = 2, + + /// Accept certificates that are expired, self-signed + /// or otherwise not valid for the server hostname. + /// + /// Alias to `AcceptInvalidCertificates` for compatibility. + AcceptInvalidCertificates2 = 3, + + /// Use configuration from the provider database. + /// If there is no provider database setting for certificate checks, + /// apply strict checks to TLS certificates. + Automatic = 4, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ConnectionCandidate { + /// Server hostname or IP address. + pub host: String, + + /// Server port. + pub port: u16, + + /// Transport layer security. + pub security: ConnectionSecurity, +} + +impl fmt::Display for ConnectionCandidate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}:{}", &self.host, self.port, self.security)?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ConfiguredServerLoginParam { + pub connection: ConnectionCandidate, + + /// Username. + pub user: String, +} + +impl fmt::Display for ConfiguredServerLoginParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.connection, &self.user)?; + Ok(()) + } +} + +pub(crate) async fn prioritize_server_login_params( + sql: &Sql, + params: &[ConfiguredServerLoginParam], + alpn: &str, +) -> Result> { + let mut res: Vec<(Option, ConfiguredServerLoginParam)> = Vec::with_capacity(params.len()); + for param in params { + let timestamp = load_connection_timestamp( + sql, + alpn, + ¶m.connection.host, + param.connection.port, + None, + ) + .await?; + res.push((timestamp, param.clone())); + } + res.sort_by_key(|(ts, _param)| std::cmp::Reverse(*ts)); + Ok(res.into_iter().map(|(_ts, param)| param).collect()) +} + +/// Login parameters saved to the database +/// after successful configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ConfiguredLoginParam { + /// `From:` address that was used at the time of configuration. + pub addr: String, + + pub imap: Vec, + + // Custom IMAP user. + // + // This overwrites autoconfig from the provider database + // if non-empty. + pub imap_user: String, + + pub imap_password: String, + + pub smtp: Vec, + + // Custom SMTP user. + // + // This overwrites autoconfig from the provider database + // if non-empty. + pub smtp_user: String, + + pub smtp_password: String, + + pub provider: Option<&'static Provider>, + + /// TLS options: whether to allow invalid certificates and/or + /// invalid hostnames + pub certificate_checks: ConfiguredCertificateChecks, + + /// If true, login via OAUTH2 (not recommended anymore) + pub oauth2: bool, +} + +/// The representation of ConfiguredLoginParam in the database, +/// saved as Json. +#[derive(Debug, Serialize, Deserialize)] +struct ConfiguredLoginParamJson { + pub addr: String, + pub imap: Vec, + pub imap_user: String, + pub imap_password: String, + pub smtp: Vec, + pub smtp_user: String, + pub smtp_password: String, + pub provider_id: Option, + pub certificate_checks: ConfiguredCertificateChecks, + pub oauth2: bool, +} + +impl fmt::Display for ConfiguredLoginParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let addr = &self.addr; + let provider_id = match self.provider { + Some(provider) => provider.id, + None => "none", + }; + let certificate_checks = self.certificate_checks; + write!(f, "{addr} imap:[")?; + let mut first = true; + for imap in &self.imap { + if !first { + write!(f, ", ")?; + } + write!(f, "{imap}")?; + first = false; + } + write!(f, "] smtp:[")?; + let mut first = true; + for smtp in &self.smtp { + if !first { + write!(f, ", ")?; + } + write!(f, "{smtp}")?; + first = false; + } + write!(f, "] provider:{provider_id} cert_{certificate_checks}")?; + Ok(()) + } +} + +impl ConfiguredLoginParam { + /// Load configured account settings from the database. + /// + /// Returns `None` if account is not configured. + pub(crate) async fn load(context: &Context) -> Result> { + let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else { + return Ok(None); + }; + + let json: Option = context + .sql + .query_get_value( + "SELECT configured_param FROM transports WHERE addr=?", + (&self_addr,), + ) + .await?; + if let Some(json) = json { + Ok(Some(Self::from_json(&json)?)) + } else { + bail!("Self address {self_addr} doesn't have a corresponding transport"); + } + } + + /// Loads legacy configured param. Only used for tests and the migration. + pub(crate) async fn load_legacy(context: &Context) -> Result> { + if !context.get_config_bool(Config::Configured).await? { + return Ok(None); + } + + let addr = context + .get_config(Config::ConfiguredAddr) + .await? + .unwrap_or_default() + .trim() + .to_string(); + + let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) = + context + .get_config_parsed::(Config::ConfiguredImapCertificateChecks) + .await? + { + num_traits::FromPrimitive::from_i32(certificate_checks) + .context("Invalid configured_imap_certificate_checks value")? + } else { + // This is true for old accounts configured using C core + // which did not check TLS certificates. + ConfiguredCertificateChecks::OldAutomatic + }; + + let send_pw = context + .get_config(Config::ConfiguredSendPw) + .await? + .context("SMTP password is not configured")?; + let mail_pw = context + .get_config(Config::ConfiguredMailPw) + .await? + .context("IMAP password is not configured")?; + + let server_flags = context + .get_config_parsed::(Config::ConfiguredServerFlags) + .await? + .unwrap_or_default(); + let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); + + let provider = context.get_configured_provider().await?; + + let imap; + let smtp; + + let mail_user = context + .get_config(Config::ConfiguredMailUser) + .await? + .unwrap_or_default(); + let send_user = context + .get_config(Config::ConfiguredSendUser) + .await? + .unwrap_or_default(); + + if let Some(provider) = provider { + let parsed_addr = EmailAddress::new(&addr).context("Bad email-address")?; + let addr_localpart = parsed_addr.local; + + if provider.server.is_empty() { + let servers = vec![ + ServerParams { + protocol: Protocol::Imap, + hostname: context + .get_config(Config::ConfiguredMailServer) + .await? + .unwrap_or_default(), + port: context + .get_config_parsed::(Config::ConfiguredMailPort) + .await? + .unwrap_or_default(), + socket: context + .get_config_parsed::(Config::ConfiguredMailSecurity) + .await? + .and_then(num_traits::FromPrimitive::from_i32) + .unwrap_or_default(), + username: mail_user.clone(), + }, + ServerParams { + protocol: Protocol::Smtp, + hostname: context + .get_config(Config::ConfiguredSendServer) + .await? + .unwrap_or_default(), + port: context + .get_config_parsed::(Config::ConfiguredSendPort) + .await? + .unwrap_or_default(), + socket: context + .get_config_parsed::(Config::ConfiguredSendSecurity) + .await? + .and_then(num_traits::FromPrimitive::from_i32) + .unwrap_or_default(), + username: send_user.clone(), + }, + ]; + let servers = expand_param_vector(servers, &addr, &parsed_addr.domain); + imap = servers + .iter() + .filter_map(|params| { + let Ok(security) = params.socket.try_into() else { + return None; + }; + if params.protocol == Protocol::Imap { + Some(ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: params.hostname.clone(), + port: params.port, + security, + }, + user: params.username.clone(), + }) + } else { + None + } + }) + .collect(); + smtp = servers + .iter() + .filter_map(|params| { + let Ok(security) = params.socket.try_into() else { + return None; + }; + if params.protocol == Protocol::Smtp { + Some(ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: params.hostname.clone(), + port: params.port, + security, + }, + user: params.username.clone(), + }) + } else { + None + } + }) + .collect(); + } else { + imap = provider + .server + .iter() + .filter_map(|server| { + if server.protocol != Protocol::Imap { + return None; + } + + let Ok(security) = server.socket.try_into() else { + return None; + }; + + Some(ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: server.hostname.to_string(), + port: server.port, + security, + }, + user: if !mail_user.is_empty() { + mail_user.clone() + } else { + match server.username_pattern { + UsernamePattern::Email => addr.to_string(), + UsernamePattern::Emaillocalpart => addr_localpart.clone(), + } + }, + }) + }) + .collect(); + smtp = provider + .server + .iter() + .filter_map(|server| { + if server.protocol != Protocol::Smtp { + return None; + } + + let Ok(security) = server.socket.try_into() else { + return None; + }; + + Some(ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: server.hostname.to_string(), + port: server.port, + security, + }, + user: if !send_user.is_empty() { + send_user.clone() + } else { + match server.username_pattern { + UsernamePattern::Email => addr.to_string(), + UsernamePattern::Emaillocalpart => addr_localpart.clone(), + } + }, + }) + }) + .collect(); + } + } else if let (Some(configured_mail_servers), Some(configured_send_servers)) = ( + context.get_config(Config::ConfiguredImapServers).await?, + context.get_config(Config::ConfiguredSmtpServers).await?, + ) { + imap = serde_json::from_str(&configured_mail_servers) + .context("Failed to parse configured IMAP servers")?; + smtp = serde_json::from_str(&configured_send_servers) + .context("Failed to parse configured SMTP servers")?; + } else { + // Load legacy settings storing a single IMAP and single SMTP server. + let mail_server = context + .get_config(Config::ConfiguredMailServer) + .await? + .unwrap_or_default(); + let mail_port = context + .get_config_parsed::(Config::ConfiguredMailPort) + .await? + .unwrap_or_default(); + + let mail_security: Socket = context + .get_config_parsed::(Config::ConfiguredMailSecurity) + .await? + .and_then(num_traits::FromPrimitive::from_i32) + .unwrap_or_default(); + + let send_server = context + .get_config(Config::ConfiguredSendServer) + .await? + .context("SMTP server is not configured")?; + let send_port = context + .get_config_parsed::(Config::ConfiguredSendPort) + .await? + .unwrap_or_default(); + let send_security: Socket = context + .get_config_parsed::(Config::ConfiguredSendSecurity) + .await? + .and_then(num_traits::FromPrimitive::from_i32) + .unwrap_or_default(); + + imap = vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: mail_server, + port: mail_port, + security: mail_security.try_into()?, + }, + user: mail_user.clone(), + }]; + smtp = vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: send_server, + port: send_port, + security: send_security.try_into()?, + }, + user: send_user.clone(), + }]; + } + + Ok(Some(ConfiguredLoginParam { + addr, + imap, + imap_user: mail_user, + imap_password: mail_pw, + smtp, + smtp_user: send_user, + smtp_password: send_pw, + certificate_checks, + provider, + oauth2, + })) + } + + pub(crate) async fn save_to_transports_table( + self, + context: &Context, + entered_param: &EnteredLoginParam, + ) -> Result<()> { + let addr = addr_normalize(&self.addr); + let provider_id = self.provider.map(|provider| provider.id); + let configured_addr = context.get_config(Config::ConfiguredAddr).await?; + if let Some(configured_addr) = &configured_addr { + ensure!( + addr_cmp(configured_addr, &addr), + "Adding a second transport is not supported right now." + ); + } + context + .sql + .execute( + "INSERT INTO transports (addr, entered_param, configured_param) + VALUES (?, ?, ?) + ON CONFLICT (addr) + DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param", + ( + self.addr.clone(), + serde_json::to_string(entered_param)?, + self.into_json()?, + ), + ) + .await?; + if configured_addr.is_none() { + // If there is no transport yet, set the new transport as the primary one + context + .sql + .set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id) + .await?; + context + .sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) + .await?; + } + Ok(()) + } + + pub(crate) fn from_json(json: &str) -> Result { + let json: ConfiguredLoginParamJson = serde_json::from_str(json)?; + + let provider = json.provider_id.and_then(|id| get_provider_by_id(&id)); + + Ok(ConfiguredLoginParam { + addr: json.addr, + imap: json.imap, + imap_user: json.imap_user, + imap_password: json.imap_password, + smtp: json.smtp, + smtp_user: json.smtp_user, + smtp_password: json.smtp_password, + provider, + certificate_checks: json.certificate_checks, + oauth2: json.oauth2, + }) + } + + pub(crate) fn into_json(self) -> Result { + let json = ConfiguredLoginParamJson { + addr: self.addr, + imap: self.imap, + imap_user: self.imap_user, + imap_password: self.imap_password, + smtp: self.smtp, + smtp_user: self.smtp_user, + smtp_password: self.smtp_password, + provider_id: self.provider.map(|p| p.id.to_string()), + certificate_checks: self.certificate_checks, + oauth2: self.oauth2, + }; + Ok(serde_json::to_string(&json)?) + } + + pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool { + let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); + match self.certificate_checks { + ConfiguredCertificateChecks::OldAutomatic => { + provider_strict_tls.unwrap_or(connected_through_proxy) + } + ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true), + ConfiguredCertificateChecks::Strict => true, + ConfiguredCertificateChecks::AcceptInvalidCertificates + | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::log::LogExt as _; + use crate::provider::get_provider_by_id; + use crate::test_utils::TestContext; + + #[test] + fn test_configured_certificate_checks_display() { + use std::string::ToString; + + assert_eq!( + "accept_invalid_certificates".to_string(), + ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string() + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_save_load_login_param() -> Result<()> { + let t = TestContext::new().await; + + let param = ConfiguredLoginParam { + addr: "alice@example.org".to_string(), + imap: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "imap.example.com".to_string(), + port: 123, + security: ConnectionSecurity::Starttls, + }, + user: "alice".to_string(), + }], + imap_user: "".to_string(), + imap_password: "foo".to_string(), + smtp: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "smtp.example.com".to_string(), + port: 456, + security: ConnectionSecurity::Tls, + }, + user: "alice@example.org".to_string(), + }], + smtp_user: "".to_string(), + smtp_password: "bar".to_string(), + provider: None, + certificate_checks: ConfiguredCertificateChecks::Strict, + oauth2: false, + }; + + param + .clone() + .save_to_transports_table(&t, &EnteredLoginParam::default()) + .await?; + let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#; + assert_eq!( + t.sql + .query_get_value::("SELECT configured_param FROM transports", ()) + .await? + .unwrap(), + expected_param + ); + assert_eq!(t.is_configured().await?, true); + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); + assert_eq!(param, loaded); + + // Legacy ConfiguredImapCertificateChecks config is ignored + t.set_config(Config::ConfiguredImapCertificateChecks, Some("999")) + .await?; + assert!(ConfiguredLoginParam::load(&t).await.is_ok()); + + // Test that we don't panic on unknown ConfiguredImapCertificateChecks values. + let wrong_param = expected_param.replace("Strict", "Stricct"); + assert_ne!(expected_param, wrong_param); + t.sql + .execute("UPDATE transports SET configured_param=?", (wrong_param,)) + .await?; + assert!(ConfiguredLoginParam::load(&t).await.is_err()); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_posteo_alias() -> Result<()> { + let t = TestContext::new().await; + + let user = "alice@posteo.de"; + + // Alice has old config with "alice@posteo.at" address + // and "alice@posteo.de" username. + t.set_config(Config::Configured, Some("1")).await?; + t.set_config(Config::ConfiguredProvider, Some("posteo")) + .await?; + t.sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at")) + .await?; + t.set_config(Config::ConfiguredMailServer, Some("posteo.de")) + .await?; + t.set_config(Config::ConfiguredMailPort, Some("993")) + .await?; + t.set_config(Config::ConfiguredMailSecurity, Some("1")) + .await?; // TLS + t.set_config(Config::ConfiguredMailUser, Some(user)).await?; + t.set_config(Config::ConfiguredMailPw, Some("foobarbaz")) + .await?; + t.set_config(Config::ConfiguredImapCertificateChecks, Some("1")) + .await?; // Strict + t.set_config(Config::ConfiguredSendServer, Some("posteo.de")) + .await?; + t.set_config(Config::ConfiguredSendPort, Some("465")) + .await?; + t.set_config(Config::ConfiguredSendSecurity, Some("1")) + .await?; // TLS + t.set_config(Config::ConfiguredSendUser, Some(user)).await?; + t.set_config(Config::ConfiguredSendPw, Some("foobarbaz")) + .await?; + t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1")) + .await?; // Strict + t.set_config(Config::ConfiguredServerFlags, Some("0")) + .await?; + + let param = ConfiguredLoginParam { + addr: "alice@posteo.at".to_string(), + imap: vec![ + ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "posteo.de".to_string(), + port: 993, + security: ConnectionSecurity::Tls, + }, + user: user.to_string(), + }, + ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "posteo.de".to_string(), + port: 143, + security: ConnectionSecurity::Starttls, + }, + user: user.to_string(), + }, + ], + imap_user: "alice@posteo.de".to_string(), + imap_password: "foobarbaz".to_string(), + smtp: vec![ + ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "posteo.de".to_string(), + port: 465, + security: ConnectionSecurity::Tls, + }, + user: user.to_string(), + }, + ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "posteo.de".to_string(), + port: 587, + security: ConnectionSecurity::Starttls, + }, + user: user.to_string(), + }, + ], + smtp_user: "alice@posteo.de".to_string(), + smtp_password: "foobarbaz".to_string(), + provider: get_provider_by_id("posteo"), + certificate_checks: ConfiguredCertificateChecks::Strict, + oauth2: false, + }; + + let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); + assert_eq!(loaded, param); + + migrate_configured_login_param(&t).await; + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); + assert_eq!(loaded, param); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_empty_server_list_legacy() -> Result<()> { + // Find a provider that does not have server list set. + // + // There is at least one such provider in the provider database. + let (domain, provider) = crate::provider::data::PROVIDER_DATA + .iter() + .find(|(_domain, provider)| provider.server.is_empty()) + .unwrap(); + + let t = TestContext::new().await; + + let addr = format!("alice@{domain}"); + + t.set_config(Config::Configured, Some("1")).await?; + t.set_config(Config::ConfiguredProvider, Some(provider.id)) + .await?; + t.sql + .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr)) + .await?; + t.set_config(Config::ConfiguredMailPw, Some("foobarbaz")) + .await?; + t.set_config(Config::ConfiguredImapCertificateChecks, Some("1")) + .await?; // Strict + t.set_config(Config::ConfiguredSendPw, Some("foobarbaz")) + .await?; + t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1")) + .await?; // Strict + t.set_config(Config::ConfiguredServerFlags, Some("0")) + .await?; + + let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap(); + assert_eq!(loaded.provider, Some(*provider)); + assert_eq!(loaded.imap.is_empty(), false); + assert_eq!(loaded.smtp.is_empty(), false); + + migrate_configured_login_param(&t).await; + + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); + assert_eq!(loaded.provider, Some(*provider)); + assert_eq!(loaded.imap.is_empty(), false); + assert_eq!(loaded.smtp.is_empty(), false); + + Ok(()) + } + + async fn migrate_configured_login_param(t: &TestContext) { + t.sql.execute("DROP TABLE transports;", ()).await.unwrap(); + t.sql.set_raw_config_int("dbversion", 130).await.unwrap(); + t.sql.run_migrations(t).await.log_err(t).ok(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_empty_server_list() -> Result<()> { + // Find a provider that does not have server list set. + // + // There is at least one such provider in the provider database. + let (domain, provider) = crate::provider::data::PROVIDER_DATA + .iter() + .find(|(_domain, provider)| provider.server.is_empty()) + .unwrap(); + + let t = TestContext::new().await; + + let addr = format!("alice@{domain}"); + + ConfiguredLoginParam { + addr: addr.clone(), + imap: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "example.org".to_string(), + port: 100, + security: ConnectionSecurity::Tls, + }, + user: addr.clone(), + }], + imap_user: addr.clone(), + imap_password: "foobarbaz".to_string(), + smtp: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "example.org".to_string(), + port: 100, + security: ConnectionSecurity::Tls, + }, + user: addr.clone(), + }], + smtp_user: addr.clone(), + smtp_password: "foobarbaz".to_string(), + provider: Some(provider), + certificate_checks: ConfiguredCertificateChecks::Automatic, + oauth2: false, + } + .save_to_transports_table(&t, &EnteredLoginParam::default()) + .await?; + + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); + assert_eq!(loaded.provider, Some(*provider)); + assert_eq!(loaded.imap.is_empty(), false); + assert_eq!(loaded.smtp.is_empty(), false); + assert_eq!(t.get_configured_provider().await?, Some(*provider)); + + Ok(()) + } +}