From 4481ab18f57d48a12d1a27f9724f74ca176c947a Mon Sep 17 00:00:00 2001 From: Alexander Krotov Date: Sun, 23 Aug 2020 00:00:00 +0300 Subject: [PATCH] configure: try multiple servers for each protocol LoginParamNew structure, which contained possible IMAP and SMTP configurations to try is replaced with uniform vectors of ServerParams structures. These vectors are initialized from provider database, online Mozilla or Outlook XML configuration or user entered parameters. During configuration, vectors of ServerParams are expanded to replace unknown values with all possible variants, which are tried one by one until configuration succeeds or all variants for a particular protocol (IMAP or SMTP) are exhausted. ServerParams structure is moved into configure submodule, and all dependencies on it outside of this submodule are removed. --- src/configure/auto_mozilla.rs | 74 ++--- src/configure/auto_outlook.rs | 301 +++++++++-------- src/configure/mod.rs | 567 ++++++++++----------------------- src/configure/server_params.rs | 115 +++++++ src/dc_tools.rs | 4 - src/login_param.rs | 43 +-- src/provider/mod.rs | 36 +-- 7 files changed, 500 insertions(+), 640 deletions(-) create mode 100644 src/configure/server_params.rs diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index 88afbb082..d0c18b4b7 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -8,10 +8,10 @@ use std::str::FromStr; use crate::context::Context; use crate::login_param::LoginParam; -use crate::provider::Socket; +use crate::provider::{Protocol, Socket}; use super::read_url::read_url; -use super::Error; +use super::{Error, ServerParams}; #[derive(Debug)] struct Server { @@ -223,52 +223,40 @@ fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result Result { +/// Parses XML into `ServerParams` vector. +fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result, Error> { let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?; - let mut login_param = LoginParam::new(); - if let Some(imap_server) = moz_ac + let res = moz_ac .incoming_servers .into_iter() - .find(|incoming_server| incoming_server.typ == "imap") - { - login_param.imap.server = imap_server.hostname; - login_param.imap.port = imap_server.port; - login_param.imap.security = imap_server.sockettype; - login_param.imap.user = imap_server.username; - } - - if let Some(smtp_server) = moz_ac - .outgoing_servers - .into_iter() - .find(|outgoing_server| outgoing_server.typ == "smtp") - { - login_param.smtp.server = smtp_server.hostname; - login_param.smtp.port = smtp_server.port; - login_param.smtp.security = smtp_server.sockettype; - login_param.smtp.user = smtp_server.username; - } - - if login_param.imap.server.is_empty() - || login_param.imap.port == 0 - || login_param.smtp.server.is_empty() - || login_param.smtp.port == 0 - { - Err(Error::IncompleteAutoconfig(login_param)) - } else { - Ok(login_param) - } + .chain(moz_ac.outgoing_servers.into_iter()) + .filter_map(|server| { + let protocol = match server.typ.as_ref() { + "imap" => Some(Protocol::IMAP), + "smtp" => Some(Protocol::SMTP), + _ => None, + }; + Some(ServerParams { + protocol: protocol?, + socket: server.sockettype, + hostname: server.hostname, + port: server.port, + username: server.username, + }) + }) + .collect(); + Ok(res) } -pub async fn moz_autoconfigure( +pub(crate) async fn moz_autoconfigure( context: &Context, url: &str, param_in: &LoginParam, -) -> Result { +) -> Result, Error> { let xml_raw = read_url(context, url).await?; - let res = parse_loginparam(¶m_in.addr, &xml_raw); + let res = parse_serverparams(¶m_in.addr, &xml_raw); if let Err(err) = &res { warn!( context, @@ -285,11 +273,13 @@ mod tests { #[test] fn test_parse_outlook_autoconfig() { let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml"); - let res = parse_loginparam("example@outlook.com", xml_raw).expect("XML parsing failed"); - assert_eq!(res.imap.server, "outlook.office365.com"); - assert_eq!(res.imap.port, 993); - assert_eq!(res.smtp.server, "smtp.office365.com"); - assert_eq!(res.smtp.port, 587); + let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed"); + assert_eq!(res[0].protocol, Protocol::IMAP); + assert_eq!(res[0].hostname, "outlook.office365.com"); + assert_eq!(res[0].port, 993); + assert_eq!(res[1].protocol, Protocol::SMTP); + assert_eq!(res[1].hostname, "smtp.office365.com"); + assert_eq!(res[1].port, 587); } #[test] diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 17622eb5c..c4a8c6a43 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -1,122 +1,194 @@ -//! Outlook's Autodiscover +//! # Outlook's Autodiscover +//! +//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover +//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used. -use quick_xml::events::BytesEnd; +use quick_xml::events::Event; + +use std::io::BufRead; use crate::context::Context; -use crate::login_param::LoginParam; -use crate::provider::Socket; +use crate::provider::{Protocol, Socket}; use super::read_url::read_url; -use super::Error; +use super::{Error, ServerParams}; -struct OutlookAutodiscover { - pub out: LoginParam, - pub out_imap_set: bool, - pub out_smtp_set: bool, - pub config_type: Option, - pub config_server: String, - pub config_port: u16, - pub config_ssl: String, - pub config_redirecturl: Option, +/// Result of parsing a single `Protocol` tag. +/// +/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox +#[derive(Debug)] +struct ProtocolTag { + /// Server type, such as "IMAP", "SMTP" or "POP3". + /// + /// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox + pub typ: String, + + /// Server identifier, hostname or IP address for IMAP and SMTP. + /// + /// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox + pub server: String, + + /// Network port. + /// + /// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox + pub port: u16, + + /// Whether connection should be secure, "on" or "off", default is "on". + /// + /// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox + pub ssl: bool, } enum ParsingResult { - LoginParam(LoginParam), + Protocols(Vec), + + /// XML redirect via `RedirectUrl` tag. RedirectUrl(String), } -fn parse_xml(xml_raw: &str) -> Result { - let mut outlk_ad = OutlookAutodiscover { - out: LoginParam::new(), - out_imap_set: false, - out_smtp_set: false, - config_type: None, - config_server: String::new(), - config_port: 0, - config_ssl: String::new(), - config_redirecturl: None, - }; - - let mut reader = quick_xml::Reader::from_str(&xml_raw); - reader.trim_text(true); +/// Parses a single Protocol section. +fn parse_protocol( + reader: &mut quick_xml::Reader, +) -> Result, quick_xml::Error> { + let mut protocol_type = None; + let mut protocol_server = None; + let mut protocol_port = None; + let mut protocol_ssl = true; let mut buf = Vec::new(); let mut current_tag: Option = None; - loop { - let event = reader - .read_event(&mut buf) - .map_err(|error| Error::InvalidXml { - position: reader.buffer_position(), - error, - })?; - - match event { - quick_xml::events::Event::Start(ref e) => { - let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase(); - + match reader.read_event(&mut buf)? { + Event::Start(ref event) => { + current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase()); + } + Event::End(ref event) => { + let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); if tag == "protocol" { - outlk_ad.config_type = None; - outlk_ad.config_server = String::new(); - outlk_ad.config_port = 0; - outlk_ad.config_ssl = String::new(); - outlk_ad.config_redirecturl = None; - + break; + } + if Some(tag) == current_tag { current_tag = None; - } else { - current_tag = Some(tag); } } - quick_xml::events::Event::End(ref e) => { - outlk_autodiscover_endtag_cb(e, &mut outlk_ad); - current_tag = None; - } - quick_xml::events::Event::Text(ref e) => { + Event::Text(ref e) => { let val = e.unescape_and_decode(&reader).unwrap_or_default(); if let Some(ref tag) = current_tag { match tag.as_str() { - "type" => { - outlk_ad.config_type = Some(val.trim().to_lowercase().to_string()) + "type" => protocol_type = Some(val.trim().to_string()), + "server" => protocol_server = Some(val.trim().to_string()), + "port" => protocol_port = Some(val.trim().parse().unwrap_or_default()), + "ssl" => { + protocol_ssl = match val.trim() { + "on" => true, + "off" => false, + _ => true, + } } - "server" => outlk_ad.config_server = val.trim().to_string(), - "port" => outlk_ad.config_port = val.trim().parse().unwrap_or_default(), - "ssl" => outlk_ad.config_ssl = val.trim().to_string(), - "redirecturl" => outlk_ad.config_redirecturl = Some(val.trim().to_string()), _ => {} }; } } - quick_xml::events::Event::Eof => break, + Event::Eof => break, + _ => {} + } + } + + if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) = + (protocol_type, protocol_server, protocol_port) + { + Ok(Some(ProtocolTag { + typ: protocol_type, + server: protocol_server, + port: protocol_port, + ssl: protocol_ssl, + })) + } else { + Ok(None) + } +} + +/// Parses `RedirectUrl` tag. +fn parse_redirecturl( + reader: &mut quick_xml::Reader, +) -> Result { + let mut buf = Vec::new(); + match reader.read_event(&mut buf)? { + Event::Text(ref e) => { + let val = e.unescape_and_decode(&reader).unwrap_or_default(); + Ok(val.trim().to_string()) + } + _ => Ok("".to_string()), + } +} + +fn parse_xml_reader( + reader: &mut quick_xml::Reader, +) -> Result { + let mut protocols = Vec::new(); + + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf)? { + Event::Start(ref e) => { + let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase(); + + if tag == "protocol" { + if let Some(protocol) = parse_protocol(reader)? { + protocols.push(protocol); + } + } else if tag == "redirecturl" { + let redirecturl = parse_redirecturl(reader)?; + return Ok(ParsingResult::RedirectUrl(redirecturl)); + } + } + Event::Eof => break, _ => (), } buf.clear(); } - // XML redirect via redirecturl - let res = if outlk_ad.config_redirecturl.is_none() - || outlk_ad.config_redirecturl.as_ref().unwrap().is_empty() - { - if outlk_ad.out.imap.server.is_empty() - || outlk_ad.out.imap.port == 0 - || outlk_ad.out.smtp.server.is_empty() - || outlk_ad.out.smtp.port == 0 - { - return Err(Error::IncompleteAutoconfig(outlk_ad.out)); - } - ParsingResult::LoginParam(outlk_ad.out) - } else { - ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap()) - }; - Ok(res) + Ok(ParsingResult::Protocols(protocols)) } -pub async fn outlk_autodiscover( +fn parse_xml(xml_raw: &str) -> Result { + let mut reader = quick_xml::Reader::from_str(&xml_raw); + reader.trim_text(true); + + parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml { + position: reader.buffer_position(), + error, + }) +} + +fn protocols_to_serverparams(protocols: Vec) -> Vec { + protocols + .into_iter() + .filter_map(|protocol| { + Some(ServerParams { + protocol: match protocol.typ.to_lowercase().as_ref() { + "imap" => Some(Protocol::IMAP), + "smtp" => Some(Protocol::SMTP), + _ => None, + }?, + socket: match protocol.ssl { + true => Socket::Automatic, + false => Socket::Plain, + }, + hostname: protocol.server, + port: protocol.port, + username: String::new(), + }) + }) + .collect() +} + +pub(crate) async fn outlk_autodiscover( context: &Context, url: &str, - _param_in: &LoginParam, -) -> Result { +) -> Result, Error> { let mut url = url.to_string(); /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */ for _i in 0..10 { @@ -127,43 +199,12 @@ pub async fn outlk_autodiscover( } match res? { ParsingResult::RedirectUrl(redirect_url) => url = redirect_url, - ParsingResult::LoginParam(login_param) => return Ok(login_param), - } - } - Err(Error::RedirectionError) -} - -fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); - - if tag == "protocol" { - if let Some(type_) = &outlk_ad.config_type { - let port = outlk_ad.config_port; - let ssl_on = outlk_ad.config_ssl == "on"; - let ssl_off = outlk_ad.config_ssl == "off"; - if type_ == "imap" && !outlk_ad.out_imap_set { - outlk_ad.out.imap.server = - std::mem::replace(&mut outlk_ad.config_server, String::new()); - outlk_ad.out.imap.port = port; - if ssl_on { - outlk_ad.out.imap.security = Socket::SSL - } else if ssl_off { - outlk_ad.out.imap.security = Socket::Plain - } - outlk_ad.out_imap_set = true - } else if type_ == "smtp" && !outlk_ad.out_smtp_set { - outlk_ad.out.smtp.server = - std::mem::replace(&mut outlk_ad.config_server, String::new()); - outlk_ad.out.smtp.port = outlk_ad.config_port; - if ssl_on { - outlk_ad.out.smtp.security = Socket::SSL - } else if ssl_off { - outlk_ad.out.smtp.security = Socket::Plain - } - outlk_ad.out_smtp_set = true + ParsingResult::Protocols(protocols) => { + return Ok(protocols_to_serverparams(protocols)); } } } + Err(Error::RedirectionError) } #[cfg(test)] @@ -184,16 +225,13 @@ mod tests { ").expect("XML is not parsed successfully"); - match res { - ParsingResult::LoginParam(_lp) => { - panic!("redirecturl is not found"); - } - ParsingResult::RedirectUrl(url) => { - assert_eq!( - url, - "https://mail.example.com/autodiscover/autodiscover.xml" - ); - } + if let ParsingResult::RedirectUrl(url) = res { + assert_eq!( + url, + "https://mail.example.com/autodiscover/autodiscover.xml" + ); + } else { + panic!("redirecturl is not found"); } } @@ -228,11 +266,16 @@ mod tests { .expect("XML is not parsed successfully"); match res { - ParsingResult::LoginParam(lp) => { - assert_eq!(lp.imap.server, "example.com"); - assert_eq!(lp.imap.port, 993); - assert_eq!(lp.smtp.server, "smtp.example.com"); - assert_eq!(lp.smtp.port, 25); + ParsingResult::Protocols(protocols) => { + assert_eq!(protocols[0].typ, "IMAP"); + assert_eq!(protocols[0].server, "example.com"); + assert_eq!(protocols[0].port, 993); + assert_eq!(protocols[0].ssl, true); + + assert_eq!(protocols[1].typ, "SMTP"); + assert_eq!(protocols[1].server, "smtp.example.com"); + assert_eq!(protocols[1].port, 25); + assert_eq!(protocols[1].ssl, false); } ParsingResult::RedirectUrl(_) => { panic!("RedirectUrl is not expected"); diff --git a/src/configure/mod.rs b/src/configure/mod.rs index a1bc57789..174351c79 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -3,6 +3,7 @@ mod auto_mozilla; mod auto_outlook; mod read_url; +mod server_params; use anyhow::{bail, ensure, Context as _, Result}; use async_std::prelude::*; @@ -13,16 +14,16 @@ use crate::constants::*; use crate::context::Context; use crate::dc_tools::*; use crate::imap::Imap; -use crate::login_param::{CertificateChecks, LoginParam, LoginParamNew, ServerParams}; +use crate::login_param::{LoginParam, ServerLoginParam}; use crate::message::Message; use crate::oauth2::*; -use crate::provider::Socket; +use crate::provider::{Protocol, Socket, UsernamePattern}; use crate::smtp::Smtp; use crate::{chat, e2ee, provider}; use auto_mozilla::moz_autoconfigure; use auto_outlook::outlk_autodiscover; -use provider::{Protocol, UsernamePattern}; +use server_params::ServerParams; macro_rules! progress { ($context:tt, $progress:expr) => { @@ -117,16 +118,33 @@ impl Context { } async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { - let mut param_autoconfig: Option = None; - let mut keep_flags = 0; - - // Read login parameters from the database progress!(ctx, 1); + + // Check basic settings. ensure!(!param.addr.is_empty(), "Please enter an email address."); + // Only check for IMAP password, SMTP password is an "advanced" setting. + ensure!(!param.imap.password.is_empty(), "Please enter a password."); + if param.smtp.password.is_empty() { + param.smtp.password = param.imap.password.clone() + } + + // Normalize authentication flags. + let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 { + DC_LP_AUTH_OAUTH2 => true, + DC_LP_AUTH_NORMAL => false, + _ => false, + }; + param.server_flags &= !(DC_LP_AUTH_FLAGS as i32); + param.server_flags |= if oauth2 { + DC_LP_AUTH_OAUTH2 as i32 + } else { + DC_LP_AUTH_NORMAL as i32 + }; + // Step 1: Load the parameters and check email-address and password - if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 { + if oauth2 { // the used oauth2 addr may differ, check this. // if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one. progress!(ctx, 10); @@ -151,93 +169,106 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> { // Step 2: Autoconfig progress!(ctx, 200); - // param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then - // param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for - // autoconfig or not + let param_autoconfig; if param.imap.server.is_empty() && param.imap.port == 0 + && param.imap.security == Socket::Automatic + && param.imap.user.is_empty() && param.smtp.server.is_empty() && param.smtp.port == 0 + && param.smtp.security == Socket::Automatic && param.smtp.user.is_empty() - && (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0 { // no advanced parameters entered by the user: query provider-database or do Autoconfig - keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2; - if let Some(new_param) = get_offline_autoconfig(ctx, ¶m) { - // got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting - param_autoconfig = Some(new_param); - } - if param_autoconfig.is_none() { + if let Some(servers) = get_offline_autoconfig(ctx, ¶m.addr) { + param_autoconfig = Some(servers); + } else { param_autoconfig = get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await; } + } else { + param_autoconfig = None; } - // C. Do we have any autoconfig result? progress!(ctx, 500); - if let Some(ref cfg) = param_autoconfig { - if let Some(cfg) = loginparam_new_to_old(ctx, cfg) { - info!(ctx, "Got autoconfig: {:?}", &cfg); - if !cfg.imap.user.is_empty() { - param.imap.user = cfg.imap.user.clone(); - } - // all other values are always NULL when entering autoconfig - param.imap.server = cfg.imap.server.clone(); - param.imap.port = cfg.imap.port; - param.imap.security = cfg.imap.security; - param.smtp.server = cfg.smtp.server.clone(); - param.smtp.port = cfg.smtp.port; - param.smtp.user = cfg.smtp.user.clone(); - param.smtp.security = cfg.smtp.security; - param.server_flags = cfg.server_flags; - // although param_autoconfig's data are no longer needed from, - // it is used to later to prevent trying variations of port/server/logins - } - } - param.server_flags |= keep_flags; - // Step 3: Fill missing fields with defaults - if param.smtp.user.is_empty() { - param.smtp.user = param.imap.user.clone(); - } - if param.smtp.password.is_empty() { - param.smtp.password = param.imap.password.clone() - } - if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) { - param.server_flags &= !(DC_LP_AUTH_FLAGS as i32); - param.server_flags |= DC_LP_AUTH_NORMAL as i32 - } - - // do we have a complete configuration? - ensure!( - !param.imap.password.is_empty() && !param.smtp.password.is_empty(), - "Account settings incomplete." - ); + let servers: Vec = param_autoconfig + .unwrap_or_else(|| { + vec![ + ServerParams { + protocol: Protocol::IMAP, + hostname: param.imap.server.clone(), + port: param.imap.port, + socket: param.imap.security, + username: param.imap.user.clone(), + }, + ServerParams { + protocol: Protocol::SMTP, + hostname: param.smtp.server.clone(), + port: param.smtp.port, + socket: param.smtp.security, + username: param.smtp.user.clone(), + }, + ] + }) + .into_iter() + // The order of expansion is important: ports are expanded the + // last, so they are changed the first. Username is only + // changed if default value (address with domain) didn't work + // for all available hosts and ports. + .flat_map(|params| params.expand_usernames(¶m.addr).into_iter()) + .flat_map(|params| params.expand_hostnames(¶m_domain).into_iter()) + .flat_map(|params| params.expand_ports().into_iter()) + .collect(); + // Configure IMAP progress!(ctx, 600); - // try to connect to IMAP - if we did not got an autoconfig, - // do some further tries with different settings and username variations let (_s, r) = async_std::sync::channel(1); let mut imap = Imap::new(r); - if param_autoconfig.is_some() { - if try_imap_one_param(ctx, ¶m, &mut imap).await.is_err() { - bail!("IMAP autoconfig did not succeed"); - } - } else { - *param = try_imap_hostnames(ctx, param.clone(), &mut imap).await?; - } - progress!(ctx, 750); + let mut imap_configured = false; + for imap_server in servers + .iter() + .filter(|params| params.protocol == Protocol::IMAP) + { + param.imap.user = imap_server.username.clone(); + param.imap.server = imap_server.hostname.clone(); + param.imap.port = imap_server.port; + param.imap.security = imap_server.socket; - let mut smtp = Smtp::new(); - if param_autoconfig.is_some() { - if try_smtp_one_param(ctx, ¶m, &mut smtp).await.is_err() { - bail!("SMTP autoconfig did not succeed"); + if try_imap_one_param(ctx, ¶m.imap, ¶m.addr, oauth2, &mut imap).await { + imap_configured = true; + break; } - } else { - *param = try_smtp_hostnames(ctx, param.clone(), &mut smtp).await?; } + if !imap_configured { + bail!("IMAP autoconfig did not succeed"); + } + + // Configure SMTP + progress!(ctx, 750); + let mut smtp = Smtp::new(); + + let mut smtp_configured = false; + for smtp_server in servers + .iter() + .filter(|params| params.protocol == Protocol::SMTP) + { + param.smtp.user = smtp_server.username.clone(); + param.smtp.server = smtp_server.hostname.clone(); + param.smtp.port = smtp_server.port; + param.smtp.security = smtp_server.socket; + + if try_smtp_one_param(ctx, ¶m.smtp, ¶m.addr, oauth2, &mut smtp).await { + smtp_configured = true; + break; + } + } + if !smtp_configured { + bail!("SMTP autoconfig did not succeed"); + } + progress!(ctx, 900); let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await @@ -306,8 +337,8 @@ impl AutoconfigSource { AutoconfigSource { provider: AutoconfigProvider::Outlook, url: format!( - "https://{}{}/autodiscover/autodiscover.xml", - "autodiscover.", domain + "https://autodiscover.{}/autodiscover/autodiscover.xml", + domain ), }, // always SSL for Thunderbird's database @@ -318,10 +349,10 @@ impl AutoconfigSource { ] } - async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result { + async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result> { let params = match self.provider { AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, ¶m).await?, - AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, ¶m).await?, + AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url).await?, }; Ok(params) @@ -337,7 +368,7 @@ async fn get_autoconfig( param: &LoginParam, param_domain: &str, param_addr_urlencoded: &str, -) -> Option { +) -> Option> { let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded); let mut progress = 300; @@ -346,344 +377,104 @@ async fn get_autoconfig( progress!(ctx, progress); progress += 10; if let Ok(res) = res { - return Some(loginparam_old_to_new(res)); + return Some(res); } } None } -fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option { +fn get_offline_autoconfig(context: &Context, addr: &str) -> Option> { info!( context, "checking internal provider-info for offline autoconfig" ); - if let Some(provider) = provider::get_provider_info(¶m.addr) { + if let Some(provider) = provider::get_provider_info(&addr) { match provider.status { provider::Status::OK | provider::Status::PREPARATION => { - let imap = provider.get_imap_server(); - let smtp = provider.get_smtp_server(); - return Some(LoginParamNew { - addr: param.addr.clone(), - imap, - smtp, - }); + if provider.server.is_empty() { + info!(context, "offline autoconfig found, but no servers defined"); + None + } else { + info!(context, "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 => addr.to_string(), + UsernamePattern::EMAILLOCALPART => { + if let Some(at) = addr.find('@') { + addr.split_at(at).0.to_string() + } else { + addr.to_string() + } + } + }, + }) + .collect(); + Some(servers) + } } provider::Status::BROKEN => { info!(context, "offline autoconfig found, provider is broken"); - return None; + None } } - } - info!(context, "no offline autoconfig found"); - None -} - -pub fn loginparam_new_to_old(context: &Context, servers: &LoginParamNew) -> Option { - let LoginParamNew { addr, imap, smtp } = servers; - if let Some(imap) = imap.get(0) { - if let Some(smtp) = smtp.get(0) { - let mut p = LoginParam::new(); - p.addr = addr.clone(); - - p.imap.server = imap.hostname.to_string(); - p.imap.user = imap.apply_username_pattern(addr.clone()); - p.imap.port = imap.port; - p.imap.security = imap.socket; - p.imap.certificate_checks = CertificateChecks::Automatic; - - p.smtp.server = smtp.hostname.to_string(); - p.smtp.user = smtp.apply_username_pattern(addr.clone()); - p.smtp.port = smtp.port; - p.smtp.security = smtp.socket; - p.smtp.certificate_checks = CertificateChecks::Automatic; - - info!(context, "offline autoconfig found: {}", p); - return Some(p); - } - } - info!(context, "offline autoconfig found, but no servers defined"); - None -} - -pub fn loginparam_old_to_new(p: LoginParam) -> LoginParamNew { - LoginParamNew { - addr: p.addr.clone(), - imap: vec![ServerParams { - protocol: Protocol::IMAP, - socket: p.imap.security, - port: p.imap.port, - hostname: p.imap.server, - username_pattern: if p.imap.user.contains('@') { - UsernamePattern::EMAIL - } else { - UsernamePattern::EMAILLOCALPART - }, - }], - smtp: vec![ServerParams { - protocol: Protocol::SMTP, - socket: p.smtp.security, - port: p.smtp.port, - hostname: p.smtp.server, - username_pattern: if p.smtp.user.contains('@') { - UsernamePattern::EMAIL - } else { - provider::UsernamePattern::EMAILLOCALPART - }, - }], - } -} - -async fn try_imap_hostnames( - context: &Context, - mut param: LoginParam, - imap: &mut Imap, -) -> Result { - if param.imap.server.is_empty() { - let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?; - let param_domain = parsed.domain; - - param.imap.server = param_domain.clone(); - if let Ok(param) = try_imap_ports(context, param.clone(), imap).await { - return Ok(param); - } - - progress!(context, 650); - param.imap.server = "imap.".to_string() + ¶m_domain; - if let Ok(param) = try_imap_ports(context, param.clone(), imap).await { - return Ok(param); - } - - progress!(context, 700); - param.imap.server = "mail.".to_string() + ¶m_domain; - try_imap_ports(context, param, imap).await } else { - progress!(context, 700); - try_imap_ports(context, param, imap).await + info!(context, "no offline autoconfig found"); + None } } -// Try various IMAP ports and corresponding TLS settings. -async fn try_imap_ports( +async fn try_imap_one_param( context: &Context, - mut param: LoginParam, + param: &ServerLoginParam, + addr: &str, + oauth2: bool, imap: &mut Imap, -) -> Result { - // Try to infer port from socket security. - if param.imap.port == 0 { - param.imap.port = match param.imap.security { - Socket::SSL => 993, - Socket::STARTTLS | Socket::Plain => 143, - Socket::Automatic => 0, - } - } - - if param.imap.port == 0 { - // Neither port nor security is set. - // - // Try common secure combinations. - - // Try TLS over port 993 - param.imap.security = Socket::SSL; - param.imap.port = 993; - if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await { - return Ok(login_param); - } - - // Try STARTTLS over port 143 - param.imap.security = Socket::STARTTLS; - param.imap.port = 143; - try_imap_usernames(context, param, imap).await - } else if param.imap.security == Socket::Automatic { - // Try TLS over user-provided port. - param.imap.security = Socket::SSL; - if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await { - return Ok(login_param); - } - - // Try STARTTLS over user-provided port. - param.imap.security = Socket::STARTTLS; - try_imap_usernames(context, param, imap).await - } else { - try_imap_usernames(context, param, imap).await - } -} - -async fn try_imap_usernames( - context: &Context, - mut param: LoginParam, - imap: &mut Imap, -) -> Result { - if param.imap.user.is_empty() { - param.imap.user = param.addr.clone(); - if let Err(e) = try_imap_one_param(context, ¶m, imap).await { - if let Some(at) = param.imap.user.find('@') { - param.imap.user = param.imap.user.split_at(at).0.to_string(); - try_imap_one_param(context, ¶m, imap).await?; - } else { - return Err(e); - } - } - Ok(param) - } else { - try_imap_one_param(context, ¶m, imap).await?; - Ok(param) - } -} - -async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> { +) -> bool { let inf = format!( - "imap: {}@{}:{} security={} certificate_checks={} flags=0x{:x}", - param.imap.user, - param.imap.server, - param.imap.port, - param.imap.security, - param.imap.certificate_checks, - param.server_flags, + "imap: {}@{}:{} security={} certificate_checks={} oauth2={}", + param.user, param.server, param.port, param.security, param.certificate_checks, oauth2 ); info!(context, "Trying: {}", inf); - if imap - .connect( - context, - ¶m.imap, - ¶m.addr, - param.server_flags & DC_LP_AUTH_OAUTH2 != 0, - ) - .await - { + if imap.connect(context, param, addr, oauth2).await { info!(context, "success: {}", inf); - return Ok(()); - } - - bail!("Could not connect: {}", inf); -} - -async fn try_smtp_hostnames( - context: &Context, - mut param: LoginParam, - smtp: &mut Smtp, -) -> Result { - if param.smtp.server.is_empty() { - let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?; - let param_domain = parsed.domain; - - param.smtp.server = param_domain.clone(); - if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await { - return Ok(param); - } - - progress!(context, 800); - param.smtp.server = "smtp.".to_string() + ¶m_domain; - if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await { - return Ok(param); - } - - progress!(context, 850); - param.smtp.server = "mail.".to_string() + ¶m_domain; - try_smtp_ports(context, param, smtp).await + true } else { - progress!(context, 850); - try_smtp_ports(context, param, smtp).await + info!(context, "failure: {}", inf); + false } } -// Try various SMTP ports and corresponding TLS settings. -async fn try_smtp_ports( +async fn try_smtp_one_param( context: &Context, - mut param: LoginParam, + param: &ServerLoginParam, + addr: &str, + oauth2: bool, smtp: &mut Smtp, -) -> Result { - // Try to infer port from socket security. - if param.smtp.port == 0 { - param.smtp.port = match param.smtp.security { - Socket::Automatic => 0, - Socket::STARTTLS | Socket::Plain => 587, - Socket::SSL => 465, - }; - } - - if param.smtp.port == 0 { - // Neither port nor security is set. - // - // Try common secure combinations. - - // Try TLS over port 465. - param.smtp.security = Socket::SSL; - param.smtp.port = 465; - if let Ok(login_param) = try_smtp_usernames(context, param.clone(), smtp).await { - return Ok(login_param); - } - - // Try STARTTLS over port 587. - param.smtp.security = Socket::STARTTLS; - param.smtp.port = 587; - try_smtp_usernames(context, param, smtp).await - } else if param.smtp.security == Socket::Automatic { - // Try TLS over user-provided port. - param.smtp.security = Socket::SSL; - if let Ok(param) = try_smtp_usernames(context, param.clone(), smtp).await { - return Ok(param); - } - - // Try STARTTLS over user-provided port. - param.smtp.security = Socket::STARTTLS; - try_smtp_usernames(context, param, smtp).await - } else { - try_smtp_usernames(context, param, smtp).await - } -} - -async fn try_smtp_usernames( - context: &Context, - mut param: LoginParam, - smtp: &mut Smtp, -) -> Result { - if param.smtp.user.is_empty() { - param.smtp.user = param.addr.clone(); - if let Err(e) = try_smtp_one_param(context, ¶m, smtp).await { - if let Some(at) = param.smtp.user.find('@') { - param.smtp.user = param.smtp.user.split_at(at).0.to_string(); - try_smtp_one_param(context, ¶m, smtp).await?; - } else { - return Err(e); - } - } - Ok(param) - } else { - try_smtp_one_param(context, ¶m, smtp).await?; - Ok(param) - } -} - -async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> { +) -> bool { let inf = format!( - "smtp: {}@{}:{} security={} certificate_checks={} flags=0x{:x}", - param.smtp.user, - param.smtp.server, - param.smtp.port, - param.smtp.security, - param.smtp.certificate_checks, - param.server_flags + "smtp: {}@{}:{} security={} certificate_checks={} oauth2={}", + param.user, param.server, param.port, param.security, param.certificate_checks, oauth2 ); info!(context, "Trying: {}", inf); - if let Err(err) = smtp - .connect( - context, - ¶m.smtp, - ¶m.addr, - param.server_flags & DC_LP_AUTH_OAUTH2 != 0, - ) - .await - { - bail!("could not connect: {}", err); + if let Err(err) = smtp.connect(context, param, addr, oauth2).await { + info!(context, "failure: {}", err); + false + } else { + info!(context, "success: {}", inf); + smtp.disconnect().await; + true } - - info!(context, "success: {}", inf); - smtp.disconnect().await; - Ok(()) } #[derive(Debug, thiserror::Error)] @@ -698,9 +489,6 @@ pub enum Error { error: quick_xml::Error, }, - #[error("Bad or incomplete autoconfig")] - IncompleteAutoconfig(LoginParam), - #[error("Failed to get URL")] ReadUrlError(#[from] self::read_url::Error), @@ -733,20 +521,15 @@ mod tests { async fn test_get_offline_autoconfig() { let context = TestContext::new().await.ctx; - let mut params = LoginParam::new(); - params.addr = "someone123@example.org".to_string(); - assert!(get_offline_autoconfig(&context, ¶ms).is_none()); + let addr = "someone123@example.org"; + assert!(get_offline_autoconfig(&context, addr).is_none()); - let mut params = LoginParam::new(); - params.addr = "someone123@nauta.cu".to_string(); - let found_params = get_offline_autoconfig(&context, ¶ms).unwrap(); - assert_eq!(found_params.imap.len(), 1); - assert_eq!(found_params.smtp.len(), 1); - assert_eq!(found_params.imap[0].hostname, "imap.nauta.cu".to_string()); - assert_eq!(found_params.smtp[0].hostname, "smtp.nauta.cu".to_string()); - - let lp_old = loginparam_new_to_old(&context, &found_params).unwrap(); - assert_eq!(lp_old.imap.certificate_checks, CertificateChecks::Automatic); - assert_eq!(lp_old.smtp.certificate_checks, CertificateChecks::Automatic); + let addr = "someone123@nauta.cu"; + let found_params = get_offline_autoconfig(&context, addr).unwrap(); + assert_eq!(found_params.len(), 2); + assert_eq!(found_params[0].protocol, Protocol::IMAP); + assert_eq!(found_params[0].hostname, "imap.nauta.cu".to_string()); + assert_eq!(found_params[1].protocol, Protocol::SMTP); + assert_eq!(found_params[1].hostname, "smtp.nauta.cu".to_string()); } } diff --git a/src/configure/server_params.rs b/src/configure/server_params.rs new file mode 100644 index 000000000..4f533d918 --- /dev/null +++ b/src/configure/server_params.rs @@ -0,0 +1,115 @@ +//! Variable server parameters lists + +use crate::provider::{Protocol, Socket}; + +/// Set of variable parameters to try during configuration. +/// +/// Can be loaded from offline provider database, online configuraiton +/// or derived from user entered parameters. +#[derive(Debug, Clone)] +pub(crate) struct ServerParams { + /// Protocol, such as IMAP or SMTP. + pub protocol: Protocol, + + /// Server hostname, empty if unknown. + pub hostname: String, + + /// Server port, zero if unknown. + pub port: u16, + + /// Socket security, such as TLS or STARTTLS, Socket::Automatic if unknown. + pub socket: Socket, + + /// Username, empty if unknown. + pub username: String, +} + +impl ServerParams { + pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec { + let mut res = Vec::new(); + + if self.username.is_empty() { + self.username = addr.to_string(); + res.push(self.clone()); + + if let Some(at) = addr.find('@') { + self.username = addr.split_at(at).0.to_string(); + res.push(self); + } + } else { + res.push(self) + } + res + } + + pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec { + let mut res = Vec::new(); + if self.hostname.is_empty() { + self.hostname = param_domain.to_string(); + res.push(self.clone()); + + self.hostname = match self.protocol { + Protocol::IMAP => "imap.".to_string() + param_domain, + Protocol::SMTP => "smtp.".to_string() + param_domain, + }; + res.push(self.clone()); + + self.hostname = "mail.".to_string() + param_domain; + res.push(self); + } else { + res.push(self); + } + res + } + + pub(crate) fn expand_ports(mut self) -> Vec { + // Try to infer port from socket security. + if self.port == 0 { + self.port = match self.socket { + Socket::SSL => match self.protocol { + Protocol::IMAP => 993, + Protocol::SMTP => 465, + }, + Socket::STARTTLS | Socket::Plain => match self.protocol { + Protocol::IMAP => 143, + Protocol::SMTP => 587, + }, + Socket::Automatic => 0, + } + } + + let mut res = Vec::new(); + if self.port == 0 { + // Neither port nor security is set. + // + // Try common secure combinations. + + // Try STARTTLS + self.socket = Socket::STARTTLS; + self.port = match self.protocol { + Protocol::IMAP => 143, + Protocol::SMTP => 587, + }; + res.push(self.clone()); + + // Try TLS + self.socket = Socket::SSL; + self.port = match self.protocol { + Protocol::IMAP => 993, + Protocol::SMTP => 465, + }; + res.push(self); + } else if self.socket == Socket::Automatic { + // Try TLS over user-provided port. + self.socket = Socket::SSL; + res.push(self.clone()); + + // Try STARTTLS over user-provided port. + self.socket = Socket::STARTTLS; + res.push(self); + } else { + res.push(self); + } + res + } +} diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 04ba79595..a6954c6b6 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -19,10 +19,6 @@ use crate::context::Context; use crate::error::{bail, Error}; use crate::events::EventType; -pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool { - 0 != v && 0 == v & (v - 1) -} - /// Shortens a string to a specified length and adds "[...]" to the /// end of the shortened string. #[allow(clippy::indexing_slicing)] diff --git a/src/login_param.rs b/src/login_param.rs index ba2666e25..3de612626 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -3,10 +3,7 @@ use std::borrow::Cow; use std::fmt; -use crate::{ - context::Context, - provider::{Protocol, Socket, UsernamePattern}, -}; +use crate::{context::Context, provider::Socket}; #[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)] #[repr(i32)] @@ -31,39 +28,6 @@ impl Default for CertificateChecks { } } -#[derive(Debug)] -pub struct ServerParams { - pub protocol: Protocol, - pub socket: Socket, - pub hostname: String, - pub port: u16, - pub username_pattern: UsernamePattern, -} - -pub type ImapServers = Vec; -pub type SmtpServers = Vec; - -#[derive(Debug)] -pub struct LoginParamNew { - pub addr: String, - pub imap: ImapServers, - pub smtp: SmtpServers, -} - -impl ServerParams { - pub fn apply_username_pattern(&self, addr: String) -> String { - match self.username_pattern { - UsernamePattern::EMAIL => addr, - UsernamePattern::EMAILLOCALPART => { - if let Some(at) = addr.find('@') { - return addr.split_at(at).0.to_string(); - } - addr - } - } - } -} - /// Login parameters for a single server, either IMAP or SMTP #[derive(Default, Debug, Clone)] pub struct ServerLoginParam { @@ -87,11 +51,6 @@ pub struct LoginParam { } impl LoginParam { - /// Create a new `LoginParam` with default values. - pub fn new() -> Self { - Default::default() - } - /// Read the login parameters from the database. pub async fn from_database(context: &Context, prefix: impl AsRef) -> Self { let prefix = prefix.as_ref(); diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 83d59e263..51055cd21 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -4,10 +4,7 @@ mod data; use crate::config::Config; use crate::dc_tools::EmailAddress; -use crate::{ - login_param::{ImapServers, ServerParams, SmtpServers}, - provider::data::PROVIDER_DATA, -}; +use crate::provider::data::PROVIDER_DATA; #[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)] #[repr(u8)] @@ -80,30 +77,6 @@ pub struct Provider { pub oauth2_authorizer: Option, } -impl Provider { - pub fn get_server(&self, protocol: Protocol) -> Vec { - self.server - .iter() - .filter(|s| s.protocol == protocol) - .map(|s| ServerParams { - protocol: s.protocol, - socket: s.socket, - hostname: s.hostname.to_string(), - port: s.port, - username_pattern: s.username_pattern.clone(), - }) - .collect() - } - - pub fn get_imap_server(&self) -> ImapServers { - self.get_server(Protocol::IMAP) - } - - pub fn get_smtp_server(&self) -> SmtpServers { - self.get_server(Protocol::SMTP) - } -} - pub fn get_provider_info(addr: &str) -> Option<&Provider> { let domain = match addr.parse::() { Ok(addr) => addr.domain, @@ -139,15 +112,16 @@ mod tests { let provider = get_provider_info("nauta.cu"); // this is no email address assert!(provider.is_none()); - let provider = get_provider_info("user@nauta.cu").unwrap(); + let addr = "user@nauta.cu"; + let provider = get_provider_info(addr).unwrap(); assert!(provider.status == Status::OK); - let server = &provider.get_imap_server()[0]; + let server = &provider.server[0]; assert_eq!(server.protocol, Protocol::IMAP); assert_eq!(server.socket, Socket::STARTTLS); assert_eq!(server.hostname, "imap.nauta.cu"); assert_eq!(server.port, 143); assert_eq!(server.username_pattern, UsernamePattern::EMAIL); - let server = &provider.get_smtp_server()[0]; + let server = &provider.server[1]; assert_eq!(server.protocol, Protocol::SMTP); assert_eq!(server.socket, Socket::STARTTLS); assert_eq!(server.hostname, "smtp.nauta.cu");