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");