From e7d4ccffe2a0181f56631efed5d9d5396f55766d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 4 Aug 2024 12:32:06 +0000 Subject: [PATCH] feat: automatic reconfiguration --- .../src/deltachat_rpc_client/direct_imap.py | 26 +- python/src/deltachat/direct_imap.py | 28 +- src/config.rs | 46 +- src/configure.rs | 462 ++++------ src/contact.rs | 12 +- src/context.rs | 10 +- src/imap.rs | 213 ++--- src/imap/client.rs | 38 +- src/login_param.rs | 822 +++++++++++++----- src/net.rs | 12 +- src/net/dns.rs | 13 +- src/qr/dclogin_scheme.rs | 17 +- src/smtp.rs | 106 +-- src/smtp/connect.rs | 85 +- 14 files changed, 1087 insertions(+), 803 deletions(-) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py b/deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py index 7d384784d..196afabae 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py @@ -9,18 +9,19 @@ import io import pathlib import ssl from contextlib import contextmanager +from typing import TYPE_CHECKING from imap_tools import ( AND, Header, MailBox, - MailBoxTls, MailMessage, MailMessageFlags, errors, ) -from . import Account, const +if TYPE_CHECKING: + from . import Account FLAGS = b"FLAGS" FETCH = b"FETCH" @@ -35,28 +36,15 @@ class DirectImap: self.connect() def connect(self): + # Assume the testing server supports TLS on port 993. host = self.account.get_config("configured_mail_server") - port = int(self.account.get_config("configured_mail_port")) - security = int(self.account.get_config("configured_mail_security")) + port = 993 user = self.account.get_config("addr") + host = user.rsplit("@")[-1] pw = self.account.get_config("mail_pw") - if security == const.SocketSecurity.PLAIN: - ssl_context = None - else: - ssl_context = ssl.create_default_context() - - # don't check if certificate hostname doesn't match target hostname - ssl_context.check_hostname = False - - # don't check if the certificate is trusted by a certificate authority - ssl_context.verify_mode = ssl.CERT_NONE - - if security == const.SocketSecurity.STARTTLS: - self.conn = MailBoxTls(host, port, ssl_context=ssl_context) - elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL: - self.conn = MailBox(host, port, ssl_context=ssl_context) + self.conn = MailBox(host, port, ssl_context=ssl.create_default_context()) self.conn.login(user, pw) self.select_folder("INBOX") diff --git a/python/src/deltachat/direct_imap.py b/python/src/deltachat/direct_imap.py index 7584bcec5..5f6323399 100644 --- a/python/src/deltachat/direct_imap.py +++ b/python/src/deltachat/direct_imap.py @@ -8,19 +8,19 @@ import io import pathlib import ssl from contextlib import contextmanager -from typing import List +from typing import List, TYPE_CHECKING from imap_tools import ( AND, Header, MailBox, - MailBoxTls, MailMessage, MailMessageFlags, errors, ) -from deltachat import Account, const +if TYPE_CHECKING: + from deltachat import Account FLAGS = b"FLAGS" FETCH = b"FETCH" @@ -28,7 +28,7 @@ ALL = "1:*" class DirectImap: - def __init__(self, account: Account) -> None: + def __init__(self, account: "Account") -> None: self.account = account self.logid = account.get_config("displayname") or id(account) self._idling = False @@ -36,27 +36,13 @@ class DirectImap: def connect(self): host = self.account.get_config("configured_mail_server") - port = int(self.account.get_config("configured_mail_port")) - security = int(self.account.get_config("configured_mail_security")) + port = 993 user = self.account.get_config("addr") + host = user.rsplit("@")[-1] pw = self.account.get_config("mail_pw") - if security == const.DC_SOCKET_PLAIN: - ssl_context = None - else: - ssl_context = ssl.create_default_context() - - # don't check if certificate hostname doesn't match target hostname - ssl_context.check_hostname = False - - # don't check if the certificate is trusted by a certificate authority - ssl_context.verify_mode = ssl.CERT_NONE - - if security == const.DC_SOCKET_STARTTLS: - self.conn = MailBoxTls(host, port, ssl_context=ssl_context) - elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL: - self.conn = MailBox(host, port, ssl_context=ssl_context) + self.conn = MailBox(host, port, ssl_context=ssl.create_default_context()) self.conn.login(user, pw) self.select_folder("INBOX") diff --git a/src/config.rs b/src/config.rs index 08c884589..ab30fbe79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -199,21 +199,32 @@ pub enum Config { /// The primary email address. Also see `SecondaryAddrs`. ConfiguredAddr, + /// List of configured IMAP servers as a JSON array. + ConfiguredImapServers, + /// Configured IMAP server hostname. + /// + /// This is replaced by `configured_imap_servers` for new configurations. ConfiguredMailServer, + /// Configured IMAP server port. + /// + /// This is replaced by `configured_imap_servers` for new configurations. + ConfiguredMailPort, + + /// Configured IMAP server security (e.g. TLS, STARTTLS). + /// + /// This is replaced by `configured_imap_servers` for new configurations. + ConfiguredMailSecurity, + /// Configured IMAP server username. + /// + /// This is replaced by `configured_imap_servers` for new configurations. ConfiguredMailUser, /// Configured IMAP server password. ConfiguredMailPw, - /// Configured IMAP server port. - ConfiguredMailPort, - - /// Configured IMAP server security (e.g. TLS, STARTTLS). - ConfiguredMailSecurity, - /// Configured TLS certificate checks. /// This option is saved on successful configuration /// and should not be modified manually. @@ -222,18 +233,32 @@ pub enum Config { /// but has "IMAP" in the name for backwards compatibility. ConfiguredImapCertificateChecks, + /// List of configured SMTP servers as a JSON array. + ConfiguredSmtpServers, + /// Configured SMTP server hostname. + /// + /// This is replaced by `configured_smtp_servers` for new configurations. ConfiguredSendServer, + /// Configured SMTP server port. + /// + /// This is replaced by `configured_smtp_servers` for new configurations. + ConfiguredSendPort, + + /// Configured SMTP server security (e.g. TLS, STARTTLS). + /// + /// This is replaced by `configured_smtp_servers` for new configurations. + ConfiguredSendSecurity, + /// Configured SMTP server username. + /// + /// This is replaced by `configured_smtp_servers` for new configurations. ConfiguredSendUser, /// Configured SMTP server password. ConfiguredSendPw, - /// Configured SMTP server port. - ConfiguredSendPort, - /// Deprecated, stored for backwards compatibility. /// /// ConfiguredImapCertificateChecks is actually used. @@ -242,9 +267,6 @@ pub enum Config { /// Whether OAuth 2 is used with configured provider. ConfiguredServerFlags, - /// Configured SMTP server security (e.g. TLS, STARTTLS). - ConfiguredSendSecurity, - /// Configured folder for incoming messages. ConfiguredInboxFolder, diff --git a/src/configure.rs b/src/configure.rs index 33f1bec50..f614e2144 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -25,14 +25,16 @@ use tokio::task; use crate::config::{self, Config}; use crate::context::Context; -use crate::imap::{session::Session as ImapSession, Imap}; +use crate::imap::Imap; use crate::log::LogExt; -use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam}; +use crate::login_param::{ + ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, + ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam, +}; use crate::message::{Message, Viewtype}; use crate::oauth2::get_oauth2_addr; use crate::provider::{Protocol, Socket, UsernamePattern}; use crate::smtp::Smtp; -use crate::socks::Socks5Config; use crate::stock_str; use crate::sync::Sync::*; use crate::tools::time; @@ -110,16 +112,15 @@ impl Context { async fn inner_configure(&self) -> Result<()> { info!(self, "Configure ..."); - let mut param = LoginParam::load_candidate_params(self).await?; + let param = EnteredLoginParam::load(self).await?; let old_addr = self.get_config(Config::ConfiguredAddr).await?; - let success = configure(self, &mut param).await; + let configured_param_res = configure(self, ¶m).await; self.set_config_internal(Config::NotifyAboutWrongPw, None) .await?; - on_configure_completed(self, param, old_addr).await?; + on_configure_completed(self, configured_param_res?, old_addr).await?; - success?; self.set_config_internal(Config::NotifyAboutWrongPw, Some("1")) .await?; Ok(()) @@ -128,7 +129,7 @@ impl Context { async fn on_configure_completed( context: &Context, - param: LoginParam, + param: ConfiguredLoginParam, old_addr: Option, ) -> Result<()> { if let Some(provider) = param.provider { @@ -178,19 +179,28 @@ async fn on_configure_completed( Ok(()) } -async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { - progress!(ctx, 1); +/// Retrieves data from autoconfig and provider database +/// to transform user-entered login parameters into complete configuration. +async fn get_configured_param( + ctx: &Context, + param: &EnteredLoginParam, +) -> Result { + ensure!(!param.addr.is_empty(), "Missing email address."); + + ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password."); + + // SMTP password is an "advanced" setting. If unset, use the same password as for IMAP. + let smtp_password = if param.smtp.password.is_empty() { + param.imap.password.clone() + } else { + param.smtp.password.clone() + }; let socks5_config = param.socks5_config.clone(); let socks5_enabled = socks5_config.is_some(); - let ctx2 = ctx.clone(); - let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await }); - - // Step 1: Load the parameters and check email-address and password - - // OAuth is always set either for both IMAP and SMTP or not at all. - if param.imap.oauth2 { + let mut addr = param.addr.clone(); + if param.oauth2 { // the used oauth2 addr may differ, check this. // if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one. progress!(ctx, 10); @@ -199,7 +209,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { .and_then(|e| e.parse().ok()) { info!(ctx, "Authorized address is {}", oauth2_addr); - param.addr = oauth2_addr; + addr = oauth2_addr; ctx.sql .set_raw_config("addr", Some(param.addr.as_str())) .await?; @@ -211,9 +221,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?; let param_domain = parsed.domain; - // Step 2: Autoconfig progress!(ctx, 200); + let provider; let param_autoconfig; if param.imap.server.is_empty() && param.imap.port == 0 @@ -225,77 +235,51 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { && param.smtp.user.is_empty() { // no advanced parameters entered by the user: query provider-database or do Autoconfig - info!( ctx, "checking internal provider-info for offline autoconfig" ); - if let Some(provider) = - provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await - { - param.provider = Some(provider); - match provider.status { - provider::Status::Ok | provider::Status::Preparation => { - if provider.server.is_empty() { - info!(ctx, "offline autoconfig found, but no servers defined"); - param_autoconfig = None; - } else { - info!(ctx, "offline autoconfig found"); - let servers = provider - .server - .iter() - .map(|s| ServerParams { - protocol: s.protocol, - socket: s.socket, - hostname: s.hostname.to_string(), - port: s.port, - username: match s.username_pattern { - UsernamePattern::Email => param.addr.to_string(), - UsernamePattern::Emaillocalpart => { - if let Some(at) = param.addr.find('@') { - param.addr.split_at(at).0.to_string() - } else { - param.addr.to_string() - } - } - }, - }) - .collect(); + provider = provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await; + if let Some(provider) = provider { + if provider.server.is_empty() { + info!(ctx, "Offline autoconfig found, but no servers defined."); + param_autoconfig = None; + } else { + info!(ctx, "Offline autoconfig found."); + let servers = provider + .server + .iter() + .map(|s| ServerParams { + protocol: s.protocol, + socket: s.socket, + hostname: s.hostname.to_string(), + port: s.port, + username: match s.username_pattern { + UsernamePattern::Email => param.addr.to_string(), + UsernamePattern::Emaillocalpart => { + if let Some(at) = param.addr.find('@') { + param.addr.split_at(at).0.to_string() + } else { + param.addr.to_string() + } + } + }, + }) + .collect(); - param_autoconfig = Some(servers) - } - } - provider::Status::Broken => { - info!(ctx, "offline autoconfig found, provider is broken"); - param_autoconfig = None; - } + param_autoconfig = Some(servers) } } else { // Try receiving autoconfig - info!(ctx, "no offline autoconfig found"); + info!(ctx, "No offline autoconfig found."); param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await; } } else { + provider = None; param_autoconfig = None; } - let user_strict_tls = match param.certificate_checks { - CertificateChecks::Automatic => None, - CertificateChecks::Strict => Some(true), - CertificateChecks::AcceptInvalidCertificates - | CertificateChecks::AcceptInvalidCertificates2 => Some(false), - }; - let provider_strict_tls = param.provider.map(|provider| provider.opt.strict_tls); - let strict_tls = user_strict_tls.or(provider_strict_tls).unwrap_or(true); - - // Do not save `CertificateChecks::Automatic` into `configured_imap_certificate_checks`. - param.certificate_checks = if strict_tls { - CertificateChecks::Strict - } else { - CertificateChecks::AcceptInvalidCertificates - }; - progress!(ctx, 500); let mut servers = param_autoconfig.unwrap_or_default(); @@ -326,107 +310,123 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain); + let configured_login_param = ConfiguredLoginParam { + addr, + 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(), + imap_password: param.imap.password.clone(), + 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(), + smtp_password, + socks5_config: param.socks5_config.clone(), + provider, + certificate_checks: match param.certificate_checks { + EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic, + EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict, + EnteredCertificateChecks::AcceptInvalidCertificates + | EnteredCertificateChecks::AcceptInvalidCertificates2 => { + ConfiguredCertificateChecks::AcceptInvalidCertificates + } + }, + oauth2: param.oauth2, + }; + Ok(configured_login_param) +} + +async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result { + progress!(ctx, 1); + + let ctx2 = ctx.clone(); + let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await }); + + let configured_param = get_configured_param(ctx, param).await?; + let strict_tls = configured_param.strict_tls(); + progress!(ctx, 550); // Spawn SMTP configuration task - let mut smtp = Smtp::new(); - + // to try SMTP while connecting to IMAP. let context_smtp = ctx.clone(); - let mut smtp_param = param.smtp.clone(); - let smtp_addr = param.addr.clone(); - let smtp_servers: Vec = servers - .iter() - .filter(|params| params.protocol == Protocol::Smtp) - .cloned() - .collect(); + let smtp_param = configured_param.smtp.clone(); + let smtp_password = configured_param.smtp_password.clone(); + let smtp_addr = configured_param.addr.clone(); + let smtp_socks5 = configured_param.socks5_config.clone(); let smtp_config_task = task::spawn(async move { - let mut smtp_configured = false; - let mut errors = Vec::new(); - for smtp_server in smtp_servers { - smtp_param.user.clone_from(&smtp_server.username); - smtp_param.server.clone_from(&smtp_server.hostname); - smtp_param.port = smtp_server.port; - smtp_param.security = smtp_server.socket; + let mut smtp = Smtp::new(); + smtp.connect( + &context_smtp, + &smtp_param, + &smtp_password, + &smtp_socks5, + &smtp_addr, + strict_tls, + configured_param.oauth2, + ) + .await?; - match try_smtp_one_param( - &context_smtp, - &smtp_param, - &socks5_config, - &smtp_addr, - strict_tls, - &mut smtp, - ) - .await - { - Ok(_) => { - smtp_configured = true; - break; - } - Err(e) => errors.push(e), - } - } - - if smtp_configured { - Ok(smtp_param) - } else { - Err(errors) - } + Ok::<(), anyhow::Error>(()) }); progress!(ctx, 600); // Configure IMAP - let mut imap: Option<(Imap, ImapSession)> = None; - let imap_servers: Vec<&ServerParams> = servers - .iter() - .filter(|params| params.protocol == Protocol::Imap) - .collect(); - let imap_servers_count = imap_servers.len(); - let mut errors = Vec::new(); - for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() { - param.imap.user.clone_from(&imap_server.username); - param.imap.server.clone_from(&imap_server.hostname); - param.imap.port = imap_server.port; - param.imap.security = imap_server.socket; - - match try_imap_one_param( - ctx, - ¶m.imap, - ¶m.socks5_config, - ¶m.addr, - strict_tls, - ) - .await - { - Ok(configured_imap) => { - imap = Some(configured_imap); - break; - } - Err(e) => errors.push(e), - } - progress!( - ctx, - 600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count - ); - } - let (mut imap, mut imap_session) = match imap { - Some(imap) => imap, - None => bail!(nicer_configuration_error(ctx, errors).await), + let (_s, r) = async_channel::bounded(1); + let mut imap = Imap::new( + configured_param.imap.clone(), + configured_param.imap_password.clone(), + configured_param.socks5_config.clone(), + &configured_param.addr, + strict_tls, + configured_param.oauth2, + r, + ); + let mut imap_session = match imap.connect(ctx).await { + Ok(session) => session, + Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await), }; progress!(ctx, 850); // Wait for SMTP configuration - match smtp_config_task.await.unwrap() { - Ok(smtp_param) => { - param.smtp = smtp_param; - } - Err(errors) => { - bail!(nicer_configuration_error(ctx, errors).await); - } - } + smtp_config_task.await.unwrap()?; progress!(ctx, 900); @@ -474,8 +474,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { } } - // the trailing underscore is correct - param.save_as_configured_params(ctx).await?; + configured_param.save_as_configured_params(ctx).await?; ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string())) .await?; @@ -493,7 +492,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { ctx.sql.set_raw_config_bool("configured", true).await?; - Ok(()) + Ok(configured_param) } /// Retrieve available autoconfigurations. @@ -502,7 +501,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { /// B. If we have no configuration yet, search configuration in Thunderbird's central database async fn get_autoconfig( ctx: &Context, - param: &LoginParam, + param: &EnteredLoginParam, param_domain: &str, ) -> Option> { let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string(); @@ -573,140 +572,19 @@ async fn get_autoconfig( None } -async fn try_imap_one_param( - context: &Context, - param: &ServerLoginParam, - socks5_config: &Option, - addr: &str, - strict_tls: bool, -) -> Result<(Imap, ImapSession), ConfigurationError> { - let inf = format!( - "imap: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}", - param.user, - param.server, - param.port, - param.security, - strict_tls, - param.oauth2, - if let Some(socks5_config) = socks5_config { - socks5_config.to_string() - } else { - "None".to_string() - } - ); - info!(context, "Trying: {}", inf); - - let (_s, r) = async_channel::bounded(1); - - let mut imap = match Imap::new(param, socks5_config.clone(), addr, strict_tls, r) { - Err(err) => { - info!(context, "failure: {:#}", err); - return Err(ConfigurationError { - config: inf, - msg: format!("{err:#}"), - }); - } - Ok(imap) => imap, - }; - - match imap.connect(context).await { - Err(err) => { - info!(context, "IMAP failure: {err:#}."); - Err(ConfigurationError { - config: inf, - msg: format!("{err:#}"), - }) - } - Ok(session) => { - info!(context, "IMAP success: {inf}."); - Ok((imap, session)) - } - } -} - -async fn try_smtp_one_param( - context: &Context, - param: &ServerLoginParam, - socks5_config: &Option, - addr: &str, - strict_tls: bool, - smtp: &mut Smtp, -) -> Result<(), ConfigurationError> { - let inf = format!( - "smtp: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}", - param.user, - param.server, - param.port, - param.security, - strict_tls, - param.oauth2, - if let Some(socks5_config) = socks5_config { - socks5_config.to_string() - } else { - "None".to_string() - } - ); - info!(context, "Trying: {}", inf); - - if let Err(err) = smtp - .connect(context, param, socks5_config, addr, strict_tls) - .await +async fn nicer_configuration_error(context: &Context, e: String) -> String { + if e.to_lowercase().contains("could not resolve") + || e.to_lowercase().contains("no dns resolution results") + || e.to_lowercase() + .contains("temporary failure in name resolution") + || e.to_lowercase().contains("name or service not known") + || e.to_lowercase() + .contains("failed to lookup address information") { - info!(context, "SMTP failure: {err:#}."); - Err(ConfigurationError { - config: inf, - msg: format!("{err:#}"), - }) - } else { - info!(context, "SMTP success: {inf}."); - smtp.disconnect(); - Ok(()) - } -} - -/// Failure to connect and login with email client configuration. -#[derive(Debug, thiserror::Error)] -#[error("Trying {config}…\nError: {msg}")] -pub struct ConfigurationError { - /// Tried configuration description. - config: String, - - /// Error message. - msg: String, -} - -async fn nicer_configuration_error(context: &Context, errors: Vec) -> String { - let first_err = if let Some(f) = errors.first() { - f - } else { - // This means configuration failed but no errors have been captured. This should never - // happen, but if it does, the user will see classic "Error: no error". - return "no error".to_string(); - }; - - if errors.iter().all(|e| { - e.msg.to_lowercase().contains("could not resolve") - || e.msg.to_lowercase().contains("no dns resolution results") - || e.msg - .to_lowercase() - .contains("temporary failure in name resolution") - || e.msg.to_lowercase().contains("name or service not known") - || e.msg - .to_lowercase() - .contains("failed to lookup address information") - }) { return stock_str::error_no_network(context).await; } - if errors.iter().all(|e| e.msg == first_err.msg) { - return first_err.msg.to_string(); - } - - errors - .iter() - .map(|e| e.to_string()) - .collect::>() - .join("\n\n") + e } #[derive(Debug, thiserror::Error)] diff --git a/src/contact.rs b/src/contact.rs index 89369a12d..f52bffb9d 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -30,7 +30,6 @@ use crate::context::Context; use crate::events::EventType; use crate::key::{load_self_public_key, DcKey, SignedPublicKey}; use crate::log::LogExt; -use crate::login_param::LoginParam; use crate::message::MessageState; use crate::mimeparser::AvatarAction; use crate::param::{Param, Params}; @@ -1191,7 +1190,10 @@ impl Contact { ); let contact = Contact::get_by_id(context, contact_id).await?; - let loginparam = LoginParam::load_configured_params(context).await?; + let addr = context + .get_config(Config::ConfiguredAddr) + .await? + .unwrap_or_default(); let peerstate = Peerstate::from_addr(context, &contact.addr).await?; let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some()) @@ -1220,8 +1222,8 @@ impl Contact { .peek_key(false) .map(|k| k.fingerprint().to_string()) .unwrap_or_default(); - if loginparam.addr < peerstate.addr { - cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, ""); + if addr < peerstate.addr { + cat_fingerprint(&mut ret, &addr, &fingerprint_self, ""); cat_fingerprint( &mut ret, &peerstate.addr, @@ -1235,7 +1237,7 @@ impl Contact { &fingerprint_other_verified, &fingerprint_other_unverified, ); - cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, ""); + cat_fingerprint(&mut ret, &addr, &fingerprint_self, ""); } Ok(ret) diff --git a/src/context.rs b/src/context.rs index 54cf31eb9..5716143f5 100644 --- a/src/context.rs +++ b/src/context.rs @@ -27,7 +27,7 @@ use crate::download::DownloadState; use crate::events::{Event, EventEmitter, EventType, Events}; use crate::imap::{FolderMeaning, Imap, ServerMetadata}; use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _}; -use crate::login_param::LoginParam; +use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam}; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::param::{Param, Params}; use crate::peer_channels::Iroh; @@ -715,8 +715,10 @@ impl Context { /// Returns information about the context as key-value pairs. pub async fn get_info(&self) -> Result> { let unset = "0"; - let l = LoginParam::load_candidate_params_unchecked(self).await?; - let l2 = LoginParam::load_configured_params(self).await?; + let l = EnteredLoginParam::load(self).await?; + let l2 = ConfiguredLoginParam::load(self) + .await? + .map_or_else(|| "Not configured".to_string(), |param| param.to_string()); let secondary_addrs = self.get_secondary_self_addrs().await?.join(", "); let displayname = self.get_config(Config::Displayname).await?; let chats = get_chat_cnt(self).await?; @@ -807,7 +809,7 @@ impl Context { res.insert("is_configured", is_configured.to_string()); res.insert("socks5_enabled", socks5_enabled.to_string()); res.insert("entered_account_settings", l.to_string()); - res.insert("used_account_settings", l2.to_string()); + res.insert("used_account_settings", l2); if let Some(server_id) = &*self.server_id.read().await { res.insert("imap_server_id", format!("{server_id:?}")); diff --git a/src/imap.rs b/src/imap.rs index 67b4d5a80..4391cf3c0 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -32,7 +32,9 @@ use crate::contact::{Contact, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::login_param::{LoginParam, ServerLoginParam}; +use crate::login_param::{ + prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam, +}; use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype}; use crate::mimeparser; use crate::oauth2::get_oauth2_access_token; @@ -73,12 +75,17 @@ pub(crate) struct Imap { addr: String, /// Login parameters. - lp: ServerLoginParam, + lp: Vec, + + /// Password. + password: String, /// SOCKS 5 configuration. socks5_config: Option, strict_tls: bool, + oauth2: bool, + login_failed_once: bool, pub(crate) connectivity: ConnectivityStore, @@ -228,31 +235,29 @@ impl Imap { /// /// `addr` is used to renew token if OAuth2 authentication is used. pub fn new( - lp: &ServerLoginParam, + lp: Vec, + password: String, socks5_config: Option, addr: &str, strict_tls: bool, + oauth2: bool, idle_interrupt_receiver: Receiver<()>, - ) -> Result { - if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() { - bail!("Incomplete IMAP connection parameters"); - } - - let imap = Imap { + ) -> Self { + Imap { idle_interrupt_receiver, addr: addr.to_string(), - lp: lp.clone(), + lp, + password, socks5_config, strict_tls, + oauth2, login_failed_once: false, connectivity: Default::default(), conn_last_try: UNIX_EPOCH, conn_backoff_ms: 0, // 1 connection per minute + a burst of 2. ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0), - }; - - Ok(imap) + } } /// Creates new disconnected IMAP client using configured parameters. @@ -260,18 +265,18 @@ impl Imap { context: &Context, idle_interrupt_receiver: Receiver<()>, ) -> Result { - if !context.is_configured().await? { - bail!("IMAP Connect without configured params"); - } - - let param = LoginParam::load_configured_params(context).await?; + let param = ConfiguredLoginParam::load(context) + .await? + .context("Not configured")?; let imap = Self::new( - ¶m.imap, + param.imap.clone(), + param.imap_password.clone(), param.socks5_config.clone(), ¶m.addr, param.strict_tls(), + param.oauth2, idle_interrupt_receiver, - )?; + ); Ok(imap) } @@ -283,10 +288,6 @@ impl Imap { /// instead if you are going to actually use connection rather than trying connection /// parameters. pub(crate) async fn connect(&mut self, context: &Context) -> Result { - if self.lp.server.is_empty() { - bail!("IMAP operation attempted while it is torn down"); - } - let now = tools::Time::now(); let until_can_send = max( min(self.conn_last_try, now) @@ -328,91 +329,107 @@ impl Imap { ); self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms); - let connection_res = Client::connect( - context, - self.lp.server.as_ref(), - self.lp.port, - self.strict_tls, - self.socks5_config.clone(), - self.lp.security, - ) - .await; - - let client = connection_res?; - self.conn_backoff_ms = BACKOFF_MIN_MS; - self.ratelimit.send(); - - let imap_user: &str = self.lp.user.as_ref(); - let imap_pw: &str = self.lp.password.as_ref(); - let oauth2 = self.lp.oauth2; - - let login_res = if oauth2 { - info!(context, "Logging into IMAP server with OAuth 2"); - let addr: &str = self.addr.as_ref(); - - let token = get_oauth2_access_token(context, addr, imap_pw, true) - .await? - .context("IMAP could not get OAUTH token")?; - let auth = OAuth2 { - user: imap_user.into(), - access_token: token, + let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?; + let mut first_error = None; + for lp in login_params { + info!(context, "IMAP trying to connect to {}.", &lp.connection); + let connection_candidate = lp.connection.clone(); + let client = match Client::connect( + context, + self.socks5_config.clone(), + self.strict_tls, + connection_candidate, + ) + .await + { + Ok(client) => client, + Err(err) => { + warn!(context, "IMAP failed to connect: {err:#}."); + first_error.get_or_insert(err); + continue; + } }; - client.authenticate("XOAUTH2", auth).await - } else { - info!(context, "Logging into IMAP server with LOGIN"); - client.login(imap_user, imap_pw).await - }; - match login_res { - Ok(session) => { - // Store server ID in the context to display in account info. - let mut lock = context.server_id.write().await; - lock.clone_from(&session.capabilities.server_id); + self.conn_backoff_ms = BACKOFF_MIN_MS; + self.ratelimit.send(); - self.login_failed_once = false; - context.emit_event(EventType::ImapConnected(format!( - "IMAP-LOGIN as {}", - self.lp.user - ))); - self.connectivity.set_connected(context).await; - info!(context, "Successfully logged into IMAP server"); - Ok(session) - } + let imap_user: &str = lp.user.as_ref(); + let imap_pw: &str = &self.password; - Err(err) => { - let imap_user = self.lp.user.to_owned(); - let message = stock_str::cannot_login(context, &imap_user).await; + let login_res = if self.oauth2 { + info!(context, "Logging into IMAP server with OAuth 2."); + let addr: &str = self.addr.as_ref(); - warn!(context, "{} ({:#})", message, err); + let token = get_oauth2_access_token(context, addr, imap_pw, true) + .await? + .context("IMAP could not get OAUTH token")?; + let auth = OAuth2 { + user: imap_user.into(), + access_token: token, + }; + client.authenticate("XOAUTH2", auth).await + } else { + info!(context, "Logging into IMAP server with LOGIN."); + client.login(imap_user, imap_pw).await + }; - let lock = context.wrong_pw_warning_mutex.lock().await; - if self.login_failed_once - && err.to_string().to_lowercase().contains("authentication") - && context.get_config_bool(Config::NotifyAboutWrongPw).await? - { - if let Err(e) = context - .set_config_internal(Config::NotifyAboutWrongPw, None) - .await - { - warn!(context, "{:#}", e); - } - drop(lock); + match login_res { + Ok(session) => { + // Store server ID in the context to display in account info. + let mut lock = context.server_id.write().await; + lock.clone_from(&session.capabilities.server_id); - let mut msg = Message::new(Viewtype::Text); - msg.text.clone_from(&message); - if let Err(e) = - chat::add_device_msg_with_importance(context, None, Some(&mut msg), true) - .await - { - warn!(context, "{:#}", e); - } - } else { - self.login_failed_once = true; + self.login_failed_once = false; + context.emit_event(EventType::ImapConnected(format!( + "IMAP-LOGIN as {}", + lp.user + ))); + self.connectivity.set_connected(context).await; + info!(context, "Successfully logged into IMAP server"); + return Ok(session); } - Err(format_err!("{}\n\n{:#}", message, err)) + Err(err) => { + let imap_user = lp.user.to_owned(); + let message = stock_str::cannot_login(context, &imap_user).await; + + let err_str = err.to_string(); + warn!(context, "IMAP failed to login: {err:#}."); + first_error.get_or_insert(format_err!("{message} ({err:#})")); + + let lock = context.wrong_pw_warning_mutex.lock().await; + if self.login_failed_once + && err_str.to_lowercase().contains("authentication") + && context.get_config_bool(Config::NotifyAboutWrongPw).await? + { + if let Err(e) = context + .set_config_internal(Config::NotifyAboutWrongPw, None) + .await + { + warn!(context, "{e:#}."); + } + drop(lock); + + let mut msg = Message::new(Viewtype::Text); + msg.text.clone_from(&message); + if let Err(e) = chat::add_device_msg_with_importance( + context, + None, + Some(&mut msg), + true, + ) + .await + { + warn!(context, "Failed to add device message: {e:#}."); + } + } else { + self.login_failed_once = true; + } + } } } + + Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided"))) } /// Prepare for IMAP operation. diff --git a/src/imap/client.rs b/src/imap/client.rs index 2034183a8..74d64c166 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; use std::ops::{Deref, DerefMut}; -use anyhow::{bail, format_err, Context as _, Result}; +use anyhow::{format_err, Context as _, Result}; use async_imap::Client as ImapClient; use async_imap::Session as ImapSession; use fast_socks5::client::Socks5Stream; @@ -10,12 +10,11 @@ use tokio::io::BufWriter; use super::capabilities::Capabilities; use super::session::Session; use crate::context::Context; +use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::session::SessionStream; use crate::net::tls::wrap_tls; -use crate::net::update_connection_history; -use crate::net::{connect_tcp_inner, connect_tls_inner}; -use crate::provider::Socket; +use crate::net::{connect_tcp_inner, connect_tls_inner, update_connection_history}; use crate::socks::Socks5Config; use crate::tools::time; @@ -109,42 +108,45 @@ impl Client { pub async fn connect( context: &Context, - host: &str, - port: u16, - strict_tls: bool, socks5_config: Option, - security: Socket, + strict_tls: bool, + candidate: ConnectionCandidate, ) -> Result { + let host = &candidate.host; + let port = candidate.port; + let security = candidate.security; if let Some(socks5_config) = socks5_config { let client = match security { - Socket::Automatic => bail!("IMAP port security is not configured"), - Socket::Ssl => { + ConnectionSecurity::Tls => { Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config) .await? } - Socket::Starttls => { + ConnectionSecurity::Starttls => { Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls) .await? } - Socket::Plain => { + ConnectionSecurity::Plain => { Client::connect_insecure_socks5(context, host, port, socks5_config).await? } }; Ok(client) } else { let mut first_error = None; - let load_cache = - strict_tls && (security == Socket::Ssl || security == Socket::Starttls); + let load_cache = match security { + ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls, + ConnectionSecurity::Plain => false, + }; for resolved_addr in lookup_host_with_cache(context, host, port, "imap", load_cache).await? { let res = match security { - Socket::Automatic => bail!("IMAP port security is not configured"), - Socket::Ssl => Client::connect_secure(resolved_addr, host, strict_tls).await, - Socket::Starttls => { + ConnectionSecurity::Tls => { + Client::connect_secure(resolved_addr, host, strict_tls).await + } + ConnectionSecurity::Starttls => { Client::connect_starttls(resolved_addr, host, strict_tls).await } - Socket::Plain => Client::connect_insecure(resolved_addr).await, + ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await, }; match res { Ok(client) => { diff --git a/src/login_param.rs b/src/login_param.rs index 35c86ec5c..5b2f8b73f 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -2,276 +2,205 @@ use std::fmt; -use anyhow::{ensure, Result}; +use anyhow::{format_err, Context as _, Result}; +use serde::{Deserialize, Serialize}; +use crate::config::Config; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; use crate::context::Context; -use crate::provider::Socket; -use crate::provider::{get_provider_by_id, Provider}; +use crate::net::load_connection_timestamp; +use crate::provider::{get_provider_by_id, Protocol, Provider, Socket, UsernamePattern}; use crate::socks::Socks5Config; +use crate::sql::Sql; +/// User-entered setting for certificate checks. +/// +/// Should be saved into `imap_certificate_checks` before running configuration. #[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] #[repr(u32)] #[strum(serialize_all = "snake_case")] -pub enum CertificateChecks { - /// Same as AcceptInvalidCertificates if stored in the database - /// as `configured_{imap,smtp}_certificate_checks`. - /// - /// Previous Delta Chat versions stored this in `configured_*` - /// if Automatic configuration - /// was selected, configuration with strict TLS checks failed - /// and configuration without strict TLS checks succeeded. - /// - /// Currently Delta Chat stores only - /// `Strict` or `AcceptInvalidCertificates` variants - /// in `configured_*` settings. - /// - /// `Automatic` in `{imap,smtp}_certificate_checks` - /// means that provider database setting should be taken. +pub enum EnteredCertificateChecks { + /// `Automatic` means that provider database setting should be taken. /// If there is no provider database setting for certificate checks, - /// `Automatic` is the same as `Strict`. + /// check certificates strictly. #[default] Automatic = 0, + /// Ensure that TLS certificate is valid for the server hostname. Strict = 1, - /// Same as AcceptInvalidCertificates - /// Previously known as AcceptInvalidHostnames, now deprecated. - AcceptInvalidCertificates2 = 2, + /// Accept certificates that are expired, self-signed + /// or otherwise not valid for the server hostname. + AcceptInvalidCertificates = 2, - AcceptInvalidCertificates = 3, + /// Alias for `AcceptInvalidCertificates` + /// for API compatibility. + AcceptInvalidCertificates2 = 3, +} + +/// Values saved into `imap_certificate_checks`. +#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] +#[repr(u32)] +#[strum(serialize_all = "snake_case")] +pub 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(Default, Debug, Clone, PartialEq, Eq)] -pub struct ServerLoginParam { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnteredServerLoginParam { + /// Server hostname or IP address. pub server: String, - pub user: String, - pub password: String, + + /// Server port. + /// + /// 0 if not specified. pub port: u16, + + /// Socket security. pub security: Socket, - pub oauth2: bool, + + /// Username. + /// + /// Empty string if not specified. + pub user: String, + + /// Password. + pub password: String, } -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct LoginParam { +/// Login parameters entered by the user. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnteredLoginParam { + /// Email address. pub addr: String, - pub imap: ServerLoginParam, - pub smtp: ServerLoginParam, - pub provider: Option<&'static Provider>, - pub socks5_config: Option, + + /// IMAP settings. + pub imap: EnteredServerLoginParam, + + /// SMTP settings. + pub smtp: EnteredServerLoginParam, /// TLS options: whether to allow invalid certificates and/or /// invalid hostnames - pub certificate_checks: CertificateChecks, + pub certificate_checks: EnteredCertificateChecks, + + pub socks5_config: Option, + + pub oauth2: bool, } -impl LoginParam { - /// Load entered (candidate) account settings - pub async fn load_candidate_params(context: &Context) -> Result { - let mut param = Self::load_candidate_params_unchecked(context).await?; - ensure!(!param.addr.is_empty(), "Missing email address."); - - // Only check for IMAP password, SMTP password is an "advanced" setting. - ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password."); - if param.smtp.password.is_empty() { - param.smtp.password.clone_from(¶m.imap.password) - } - Ok(param) - } - - /// Load entered (candidate) account settings without validation. - /// - /// This will result in a potentially invalid [`LoginParam`] struct as the values are - /// not validated. Only use this if you want to show this directly to the user e.g. in - /// [`Context::get_info`]. - pub async fn load_candidate_params_unchecked(context: &Context) -> Result { - LoginParam::from_database(context, "").await - } - - /// Load configured (working) account settings - pub async fn load_configured_params(context: &Context) -> Result { - LoginParam::from_database(context, "configured_").await - } - - /// Read the login parameters from the database. - async fn from_database(context: &Context, prefix: &str) -> Result { +impl EnteredLoginParam { + /// Loads entered account settings. + pub async fn load(context: &Context) -> Result { let sql = &context.sql; - let key = &format!("{prefix}addr"); let addr = sql - .get_raw_config(key) + .get_raw_config("addr") .await? .unwrap_or_default() .trim() .to_string(); - let key = &format!("{prefix}mail_server"); - let mail_server = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}mail_port"); - let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}mail_user"); - let mail_user = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}mail_pw"); - let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}mail_security"); + let mail_server = sql.get_raw_config("mail_server").await?.unwrap_or_default(); + let mail_port = sql + .get_raw_config_int("mail_port") + .await? + .unwrap_or_default(); let mail_security = sql - .get_raw_config_int(key) + .get_raw_config_int("mail_security") .await? .and_then(num_traits::FromPrimitive::from_i32) .unwrap_or_default(); + let mail_user = sql.get_raw_config("mail_user").await?.unwrap_or_default(); + let mail_pw = sql.get_raw_config("mail_pw").await?.unwrap_or_default(); // The setting is named `imap_certificate_checks` // for backwards compatibility, // but now it is a global setting applied to all protocols, // while `smtp_certificate_checks` is ignored. - let key = &format!("{prefix}imap_certificate_checks"); - let certificate_checks = - if let Some(certificate_checks) = sql.get_raw_config_int(key).await? { - num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() - } else { - Default::default() - }; + let certificate_checks = if let Some(certificate_checks) = + sql.get_raw_config_int("imap_ceritifacte_checks").await? + { + num_traits::FromPrimitive::from_i32(certificate_checks).unwrap() + } else { + Default::default() + }; - let key = &format!("{prefix}send_server"); - let send_server = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}send_port"); - let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}send_user"); - let send_user = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}send_pw"); - let send_pw = sql.get_raw_config(key).await?.unwrap_or_default(); - - let key = &format!("{prefix}send_security"); + let send_server = sql.get_raw_config("send_server").await?.unwrap_or_default(); + let send_port = sql + .get_raw_config_int("send_port") + .await? + .unwrap_or_default(); let send_security = sql - .get_raw_config_int(key) + .get_raw_config_int("send_security") .await? .and_then(num_traits::FromPrimitive::from_i32) .unwrap_or_default(); + let send_user = sql.get_raw_config("send_user").await?.unwrap_or_default(); + let send_pw = sql.get_raw_config("send_pw").await?.unwrap_or_default(); - let key = &format!("{prefix}server_flags"); - let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default(); - let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); - - let key = &format!("{prefix}provider"); - let provider = sql - .get_raw_config(key) + let server_flags = sql + .get_raw_config_int("server_flags") .await? - .and_then(|provider_id| get_provider_by_id(&provider_id)); + .unwrap_or_default(); + let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); let socks5_config = Socks5Config::from_database(&context.sql).await?; - Ok(LoginParam { + Ok(EnteredLoginParam { addr, - imap: ServerLoginParam { + imap: EnteredServerLoginParam { server: mail_server, - user: mail_user, - password: mail_pw, port: mail_port as u16, security: mail_security, - oauth2, + user: mail_user, + password: mail_pw, }, - smtp: ServerLoginParam { + smtp: EnteredServerLoginParam { server: send_server, - user: send_user, - password: send_pw, port: send_port as u16, security: send_security, - oauth2, + user: send_user, + password: send_pw, }, certificate_checks, - provider, socks5_config, + oauth2, }) } - - /// Save this loginparam to the database. - pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> { - let prefix = "configured_"; - let sql = &context.sql; - - context.set_primary_self_addr(&self.addr).await?; - - let key = &format!("{prefix}mail_server"); - sql.set_raw_config(key, Some(&self.imap.server)).await?; - - let key = &format!("{prefix}mail_port"); - sql.set_raw_config_int(key, i32::from(self.imap.port)) - .await?; - - let key = &format!("{prefix}mail_user"); - sql.set_raw_config(key, Some(&self.imap.user)).await?; - - let key = &format!("{prefix}mail_pw"); - sql.set_raw_config(key, Some(&self.imap.password)).await?; - - let key = &format!("{prefix}mail_security"); - sql.set_raw_config_int(key, self.imap.security as i32) - .await?; - - let key = &format!("{prefix}imap_certificate_checks"); - sql.set_raw_config_int(key, self.certificate_checks as i32) - .await?; - - let key = &format!("{prefix}send_server"); - sql.set_raw_config(key, Some(&self.smtp.server)).await?; - - let key = &format!("{prefix}send_port"); - sql.set_raw_config_int(key, i32::from(self.smtp.port)) - .await?; - - let key = &format!("{prefix}send_user"); - sql.set_raw_config(key, Some(&self.smtp.user)).await?; - - let key = &format!("{prefix}send_pw"); - sql.set_raw_config(key, Some(&self.smtp.password)).await?; - - let key = &format!("{prefix}send_security"); - sql.set_raw_config_int(key, self.smtp.security as i32) - .await?; - - // This is only saved for compatibility reasons, but never loaded. - let key = &format!("{prefix}smtp_certificate_checks"); - sql.set_raw_config_int(key, self.certificate_checks as i32) - .await?; - - // The OAuth2 flag is either set for both IMAP and SMTP or not at all. - let key = &format!("{prefix}server_flags"); - let server_flags = match self.imap.oauth2 { - true => DC_LP_AUTH_OAUTH2, - false => DC_LP_AUTH_NORMAL, - }; - sql.set_raw_config_int(key, server_flags).await?; - - let key = &format!("{prefix}provider"); - sql.set_raw_config(key, self.provider.map(|provider| provider.id)) - .await?; - - Ok(()) - } - - pub fn strict_tls(&self) -> bool { - let user_strict_tls = match self.certificate_checks { - CertificateChecks::Automatic => None, - CertificateChecks::Strict => Some(true), - CertificateChecks::AcceptInvalidCertificates - | CertificateChecks::AcceptInvalidCertificates2 => Some(false), - }; - let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); - user_strict_tls - .or(provider_strict_tls) - .unwrap_or(self.socks5_config.is_some()) - } } -impl fmt::Display for LoginParam { +impl fmt::Display for EnteredLoginParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let unset = "0"; let pw = "***"; @@ -289,11 +218,7 @@ impl fmt::Display for LoginParam { unset_empty(&self.imap.server), self.imap.port, self.imap.security, - if self.imap.oauth2 { - "OAUTH2" - } else { - "AUTH_NORMAL" - }, + if self.oauth2 { "OAUTH2" } else { "AUTH_NORMAL" }, unset_empty(&self.smtp.user), if !self.smtp.password.is_empty() { pw @@ -303,11 +228,7 @@ impl fmt::Display for LoginParam { unset_empty(&self.smtp.server), self.smtp.port, self.smtp.security, - if self.smtp.oauth2 { - "OAUTH2" - } else { - "AUTH_NORMAL" - }, + if self.oauth2 { "OAUTH2" } else { "AUTH_NORMAL" }, self.certificate_checks ) } @@ -321,6 +242,428 @@ fn unset_empty(s: &str) -> &str { } } +#[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 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 struct ConfiguredLoginParam { + /// `From:` address that was used at the time of configuration. + pub addr: String, + + pub imap: Vec, + + pub imap_password: String, + + pub smtp: Vec, + + pub smtp_password: String, + + pub socks5_config: Option, + + pub provider: Option<&'static Provider>, + + /// TLS options: whether to allow invalid certificates and/or + /// invalid hostnames + 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 async fn load(context: &Context) -> Result> { + let sql = &context.sql; + + if !context.get_config_bool(Config::Configured).await? { + return Ok(None); + } + + let addr = sql + .get_raw_config("configured_addr") + .await? + .unwrap_or_default() + .trim() + .to_string(); + + let certificate_checks: ConfiguredCertificateChecks = if let Some(certificate_checks) = sql + .get_raw_config_int("configured_imap_certificate_checks") + .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 = sql + .get_raw_config_int("configured_server_flags") + .await? + .unwrap_or_default(); + let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); + + let provider = context + .get_config(Config::ConfiguredProvider) + .await? + .and_then(|provider_id| get_provider_by_id(&provider_id)); + + let imap; + let smtp; + + if let Some(provider) = provider { + let addr_localpart = if let Some(at) = addr.find('@') { + addr.split_at(at).0.to_string() + } else { + addr.to_string() + }; + 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: 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: 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 = sql + .get_raw_config("configured_mail_server") + .await? + .unwrap_or_default(); + let mail_port = sql + .get_raw_config_int("configured_mail_port") + .await? + .unwrap_or_default(); + + let mail_user = sql + .get_raw_config("configured_mail_user") + .await? + .unwrap_or_default(); + let mail_security: Socket = sql + .get_raw_config_int("configured_mail_security") + .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 = sql + .get_raw_config_int("configured_send_port") + .await? + .unwrap_or_default(); + let send_user = sql + .get_raw_config("configured_send_user") + .await? + .unwrap_or_default(); + let send_security: Socket = sql + .get_raw_config_int("configured_send_security") + .await? + .and_then(num_traits::FromPrimitive::from_i32) + .unwrap_or_default(); + + imap = vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: mail_server, + port: mail_port as u16, + security: mail_security.try_into()?, + }, + user: mail_user, + }]; + smtp = vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: send_server, + port: send_port as u16, + security: send_security.try_into()?, + }, + user: send_user, + }]; + } + + let socks5_config = Socks5Config::from_database(&context.sql).await?; + + Ok(Some(ConfiguredLoginParam { + addr, + imap, + imap_password: mail_pw, + smtp, + smtp_password: send_pw, + certificate_checks, + provider, + socks5_config, + oauth2, + })) + } + + /// Save this loginparam to the database. + pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> { + let sql = &context.sql; + + context.set_primary_self_addr(&self.addr).await?; + + context + .set_config( + Config::ConfiguredImapServers, + Some(&serde_json::to_string(&self.imap)?), + ) + .await?; + context + .set_config( + Config::ConfiguredSmtpServers, + Some(&serde_json::to_string(&self.smtp)?), + ) + .await?; + + context + .set_config(Config::ConfiguredMailPw, Some(&self.imap_password)) + .await?; + context + .set_config(Config::ConfiguredSendPw, Some(&self.smtp_password)) + .await?; + + sql.set_raw_config_int( + "configured_imap_certificate_checks", + self.certificate_checks as i32, + ) + .await?; + sql.set_raw_config_int( + "configured_smtp_certificate_checks", + self.certificate_checks as i32, + ) + .await?; + + // Remove legacy settings. + context + .set_config(Config::ConfiguredMailServer, None) + .await?; + context.set_config(Config::ConfiguredMailPort, None).await?; + context + .set_config(Config::ConfiguredMailSecurity, None) + .await?; + context.set_config(Config::ConfiguredMailUser, None).await?; + context + .set_config(Config::ConfiguredSendServer, None) + .await?; + context.set_config(Config::ConfiguredSendPort, None).await?; + context + .set_config(Config::ConfiguredSendSecurity, None) + .await?; + context.set_config(Config::ConfiguredSendUser, None).await?; + + let server_flags = match self.oauth2 { + true => DC_LP_AUTH_OAUTH2, + false => DC_LP_AUTH_NORMAL, + }; + sql.set_raw_config_int("configured_server_flags", server_flags) + .await?; + + sql.set_raw_config( + "configured_provider", + self.provider.map(|provider| provider.id), + ) + .await?; + + Ok(()) + } + + pub fn strict_tls(&self) -> bool { + let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); + match self.certificate_checks { + ConfiguredCertificateChecks::OldAutomatic => { + provider_strict_tls.unwrap_or(self.socks5_config.is_some()) + } + ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true), + ConfiguredCertificateChecks::Strict => true, + ConfiguredCertificateChecks::AcceptInvalidCertificates + | ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -332,7 +675,12 @@ mod tests { assert_eq!( "accept_invalid_certificates".to_string(), - CertificateChecks::AcceptInvalidCertificates.to_string() + EnteredCertificateChecks::AcceptInvalidCertificates.to_string() + ); + + assert_eq!( + "accept_invalid_certificates".to_string(), + ConfiguredCertificateChecks::AcceptInvalidCertificates.to_string() ); } @@ -340,42 +688,42 @@ mod tests { async fn test_save_load_login_param() -> Result<()> { let t = TestContext::new().await; - let param = LoginParam { + let param = ConfiguredLoginParam { addr: "alice@example.org".to_string(), - imap: ServerLoginParam { - server: "imap.example.com".to_string(), + imap: vec![ConfiguredServerLoginParam { + connection: ConnectionCandidate { + host: "imap.example.com".to_string(), + port: 123, + security: ConnectionSecurity::Starttls, + }, user: "alice".to_string(), - password: "foo".to_string(), - port: 123, - security: Socket::Starttls, - oauth2: false, - }, - smtp: ServerLoginParam { - server: "smtp.example.com".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(), - password: "bar".to_string(), - port: 456, - security: Socket::Ssl, - oauth2: false, - }, - provider: get_provider_by_id("example.com"), + }], + smtp_password: "bar".to_string(), // socks5_config is not saved by `save_to_database`, using default value socks5_config: None, - certificate_checks: CertificateChecks::Strict, - }; - - param.save_as_configured_params(&t).await?; - let loaded = LoginParam::load_configured_params(&t).await?; - assert_eq!(param, loaded); - - // Remove provider. - let param = LoginParam { provider: None, - ..param + certificate_checks: ConfiguredCertificateChecks::Strict, + oauth2: false, }; + param.save_as_configured_params(&t).await?; - let loaded = LoginParam::load_configured_params(&t).await?; + assert_eq!( + t.get_config(Config::ConfiguredImapServers).await?.unwrap(), + r#"[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}]"# + ); + t.set_config(Config::Configured, Some("1")).await?; + let loaded = ConfiguredLoginParam::load(&t).await?.unwrap(); assert_eq!(param, loaded); + Ok(()) } } diff --git a/src/net.rs b/src/net.rs index e8e167576..cccba3e66 100644 --- a/src/net.rs +++ b/src/net.rs @@ -10,6 +10,7 @@ use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; use crate::context::Context; +use crate::sql::Sql; use crate::tools::time; pub(crate) mod dns; @@ -64,21 +65,22 @@ pub(crate) async fn update_connection_history( Ok(()) } +/// Returns timestamp of the most recent successful connection +/// to the host and port for given protocol. pub(crate) async fn load_connection_timestamp( - context: &Context, + sql: &Sql, alpn: &str, host: &str, port: u16, - addr: &str, + addr: Option<&str>, ) -> Result> { - let timestamp = context - .sql + let timestamp = sql .query_get_value( "SELECT timestamp FROM connection_history WHERE host = ? AND port = ? AND alpn = ? - AND addr = ?", + AND addr = IFNULL(?, addr)", (host, port, alpn, addr), ) .await?; diff --git a/src/net/dns.rs b/src/net/dns.rs index aa5e9f281..8de73d601 100644 --- a/src/net/dns.rs +++ b/src/net/dns.rs @@ -230,11 +230,16 @@ async fn sort_by_connection_timestamp( alpn: &str, host: &str, ) -> Result> { - let mut res: Vec<(Option, SocketAddr)> = Vec::new(); + let mut res: Vec<(Option, SocketAddr)> = Vec::with_capacity(input.len()); for addr in input { - let timestamp = - load_connection_timestamp(context, alpn, host, addr.port(), &addr.ip().to_string()) - .await?; + let timestamp = load_connection_timestamp( + &context.sql, + alpn, + host, + addr.port(), + Some(&addr.ip().to_string()), + ) + .await?; res.push((timestamp, addr)); } res.sort_by_key(|(ts, _addr)| std::cmp::Reverse(*ts)); diff --git a/src/qr/dclogin_scheme.rs b/src/qr/dclogin_scheme.rs index 8fb5c438a..cb705ae18 100644 --- a/src/qr/dclogin_scheme.rs +++ b/src/qr/dclogin_scheme.rs @@ -8,7 +8,7 @@ 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::login_param::EnteredCertificateChecks; use crate::provider::Socket; /// Options for `dclogin:` scheme. @@ -55,7 +55,7 @@ pub enum LoginOptions { smtp_security: Option, /// Certificate checks. - certificate_checks: Option, + certificate_checks: Option, }, } @@ -146,11 +146,12 @@ fn parse_socket_security(security: Option<&String>) -> Result> { fn parse_certificate_checks( certificate_checks: Option<&String>, -) -> Result> { +) -> Result> { Ok(match certificate_checks.map(|s| s.as_str()) { - Some("0") => Some(CertificateChecks::Automatic), - Some("1") => Some(CertificateChecks::Strict), - Some("3") => Some(CertificateChecks::AcceptInvalidCertificates), + Some("0") => Some(EnteredCertificateChecks::Automatic), + Some("1") => Some(EnteredCertificateChecks::Strict), + Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates), + Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2), Some(other) => bail!("Unknown certificatecheck level: {}", other), None => None, }) @@ -263,7 +264,7 @@ mod test { use anyhow::bail; use super::{decode_login, LoginOptions}; - use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr}; + use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr}; macro_rules! login_options_just_pw { ($pw: expr) => { @@ -386,7 +387,7 @@ mod test { smtp_username: Some("max@host.tld".to_owned()), smtp_password: Some("3242HS".to_owned()), smtp_security: Some(Socket::Plain), - certificate_checks: Some(CertificateChecks::Strict), + certificate_checks: Some(EnteredCertificateChecks::Strict), } ); } else { diff --git a/src/smtp.rs b/src/smtp.rs index 1c7db83ef..59c2fb59b 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -5,7 +5,7 @@ pub mod send; use anyhow::{bail, format_err, Context as _, Error, Result}; use async_smtp::response::{Category, Code, Detail}; -use async_smtp::{self as smtp, EmailAddress, SmtpTransport}; +use async_smtp::{EmailAddress, SmtpTransport}; use tokio::task; use crate::chat::{add_info_msg_with_cmd, ChatId}; @@ -13,12 +13,12 @@ use crate::config::Config; use crate::contact::{Contact, ContactId}; use crate::context::Context; use crate::events::EventType; -use crate::login_param::{LoginParam, ServerLoginParam}; +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; use crate::net::session::SessionBufStream; -use crate::oauth2::get_oauth2_access_token; use crate::scheduler::connectivity::ConnectivityStore; use crate::socks::Socks5Config; use crate::sql; @@ -88,96 +88,76 @@ impl Smtp { } self.connectivity.set_connecting(context).await; - let lp = LoginParam::load_configured_params(context).await?; + let lp = ConfiguredLoginParam::load(context) + .await? + .context("Not configured")?; self.connect( context, &lp.smtp, + &lp.smtp_password, &lp.socks5_config, &lp.addr, lp.strict_tls(), + lp.oauth2, ) .await } /// Connect using the provided login params. + #[allow(clippy::too_many_arguments)] pub async fn connect( &mut self, context: &Context, - lp: &ServerLoginParam, + login_params: &[ConfiguredServerLoginParam], + password: &str, socks5_config: &Option, addr: &str, strict_tls: bool, + oauth2: bool, ) -> Result<()> { if self.is_connected() { warn!(context, "SMTP already connected."); return Ok(()); } - if lp.server.is_empty() || lp.port == 0 { - bail!("bad connection parameters"); - } - let from = EmailAddress::new(addr.to_string()) - .with_context(|| format!("invalid login address {addr}"))?; - + .with_context(|| format!("Invalid address {addr:?}"))?; self.from = Some(from); - let domain = &lp.server; - let port = lp.port; - - let session_stream = connect::connect_stream( - context, - domain, - port, - strict_tls, - socks5_config.clone(), - lp.security, - ) - .await?; - let client = smtp::SmtpClient::new().smtp_utf8(true).without_greeting(); - let mut transport = SmtpTransport::new(client, session_stream).await?; - - // Authenticate. - { - let (creds, mechanism) = if lp.oauth2 { - // oauth2 - let send_pw = &lp.password; - let access_token = get_oauth2_access_token(context, addr, send_pw, false).await?; - if access_token.is_none() { - bail!("SMTP OAuth 2 error {}", addr); + let login_params = + prioritize_server_login_params(&context.sql, login_params, "smtp").await?; + for lp in login_params { + info!(context, "SMTP trying to connect to {}.", &lp.connection); + let transport = match connect::connect_and_auth( + context, + socks5_config, + strict_tls, + lp.connection.clone(), + oauth2, + addr, + &lp.user, + password, + ) + .await + { + Ok(transport) => transport, + Err(err) => { + warn!(context, "SMTP failed to connect: {err:#}."); + continue; } - let user = &lp.user; - ( - smtp::authentication::Credentials::new( - user.to_string(), - access_token.unwrap_or_default(), - ), - vec![smtp::authentication::Mechanism::Xoauth2], - ) - } else { - // plain - let user = lp.user.clone(); - let pw = lp.password.clone(); - ( - smtp::authentication::Credentials::new(user, pw), - vec![ - smtp::authentication::Mechanism::Plain, - smtp::authentication::Mechanism::Login, - ], - ) }; - transport.try_login(&creds, &mechanism).await?; + + self.transport = Some(transport); + self.last_success = Some(tools::Time::now()); + + context.emit_event(EventType::SmtpConnected(format!( + "SMTP-LOGIN as {} ok", + lp.user, + ))); + return Ok(()); } - self.transport = Some(transport); - self.last_success = Some(tools::Time::now()); - - context.emit_event(EventType::SmtpConnected(format!( - "SMTP-LOGIN as {} ok", - lp.user, - ))); - - Ok(()) + Err(format_err!("SMTP failed to connect")) } } diff --git a/src/smtp/connect.rs b/src/smtp/connect.rs index 788564e2d..e1ed0476a 100644 --- a/src/smtp/connect.rs +++ b/src/smtp/connect.rs @@ -7,12 +7,12 @@ use async_smtp::{SmtpClient, SmtpTransport}; use tokio::io::BufStream; use crate::context::Context; +use crate::login_param::{ConnectionCandidate, ConnectionSecurity}; use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp}; use crate::net::session::SessionBufStream; use crate::net::tls::wrap_tls; -use crate::net::update_connection_history; -use crate::net::{connect_tcp_inner, connect_tls_inner}; -use crate::provider::Socket; +use crate::net::{connect_tcp_inner, connect_tls_inner, update_connection_history}; +use crate::oauth2::get_oauth2_access_token; use crate::socks::Socks5Config; use crate::tools::time; @@ -26,6 +26,52 @@ fn alpn(port: u16) -> &'static [&'static str] { } } +#[allow(clippy::too_many_arguments)] +pub(crate) async fn connect_and_auth( + context: &Context, + socks5_config: &Option, + strict_tls: bool, + candidate: ConnectionCandidate, + oauth2: bool, + addr: &str, + user: &str, + password: &str, +) -> Result>> { + let session_stream = + connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?; + let client = async_smtp::SmtpClient::new() + .smtp_utf8(true) + .without_greeting(); + let mut transport = SmtpTransport::new(client, session_stream).await?; + + // Authenticate. + let (creds, mechanism) = if oauth2 { + // oauth2 + let access_token = get_oauth2_access_token(context, addr, password, false).await?; + if access_token.is_none() { + bail!("SMTP OAuth 2 error {}", addr); + } + ( + async_smtp::authentication::Credentials::new( + user.to_string(), + access_token.unwrap_or_default(), + ), + vec![async_smtp::authentication::Mechanism::Xoauth2], + ) + } else { + // plain + ( + async_smtp::authentication::Credentials::new(user.to_string(), password.to_string()), + vec![ + async_smtp::authentication::Mechanism::Plain, + async_smtp::authentication::Mechanism::Login, + ], + ) + }; + transport.try_login(&creds, &mechanism).await?; + Ok(transport) +} + /// Returns TLS, STARTTLS or plaintext connection /// using SOCKS5 or direct connection depending on the given configuration. /// @@ -34,41 +80,46 @@ fn alpn(port: u16) -> &'static [&'static str] { /// does not send welcome message over TLS connection /// after establishing it, welcome message is always ignored /// to unify the result regardless of whether TLS or STARTTLS is used. -pub(crate) async fn connect_stream( +async fn connect_stream( context: &Context, - host: &str, - port: u16, - strict_tls: bool, socks5_config: Option, - security: Socket, + strict_tls: bool, + candidate: ConnectionCandidate, ) -> Result> { + let host = &candidate.host; + let port = candidate.port; + let security = candidate.security; + if let Some(socks5_config) = socks5_config { let stream = match security { - Socket::Automatic => bail!("SMTP port security is not configured"), - Socket::Ssl => { + ConnectionSecurity::Tls => { connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone()) .await? } - Socket::Starttls => { + ConnectionSecurity::Starttls => { connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone()) .await? } - Socket::Plain => { + ConnectionSecurity::Plain => { connect_insecure_socks5(context, host, port, socks5_config.clone()).await? } }; Ok(stream) } else { let mut first_error = None; - let load_cache = strict_tls && (security == Socket::Ssl || security == Socket::Starttls); + let load_cache = match security { + ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls, + ConnectionSecurity::Plain => false, + }; for resolved_addr in lookup_host_with_cache(context, host, port, "smtp", load_cache).await? { let res = match security { - Socket::Automatic => bail!("SMTP port security is not configured"), - Socket::Ssl => connect_secure(resolved_addr, host, strict_tls).await, - Socket::Starttls => connect_starttls(resolved_addr, host, strict_tls).await, - Socket::Plain => connect_insecure(resolved_addr).await, + ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await, + ConnectionSecurity::Starttls => { + connect_starttls(resolved_addr, host, strict_tls).await + } + ConnectionSecurity::Plain => connect_insecure(resolved_addr).await, }; match res { Ok(stream) => {