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.
This commit is contained in:
Alexander Krotov
2020-08-23 00:00:00 +03:00
committed by link2xt
parent 927c7eb59d
commit 4481ab18f5
7 changed files with 500 additions and 640 deletions

View File

@@ -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<MozAutoco
})
}
/// Parses XML into `LoginParam` structure.
fn parse_loginparam(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
/// Parses XML into `ServerParams` vector.
fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, 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<LoginParam, Error> {
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_loginparam(&param_in.addr, &xml_raw);
let res = parse_serverparams(&param_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]

View File

@@ -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<String>,
pub config_server: String,
pub config_port: u16,
pub config_ssl: String,
pub config_redirecturl: Option<String>,
/// 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<ProtocolTag>),
/// XML redirect via `RedirectUrl` tag.
RedirectUrl(String),
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
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<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<Option<ProtocolTag>, 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<String> = 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<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<String, quick_xml::Error> {
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<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<ParsingResult, quick_xml::Error> {
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<ParsingResult, Error> {
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<ProtocolTag>) -> Vec<ServerParams> {
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<LoginParam, Error> {
) -> Result<Vec<ServerParams>, 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 {
</Response>
</Autodiscover>
").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");

View File

@@ -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<LoginParamNew> = 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, &param) {
// 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, &param.addr) {
param_autoconfig = Some(servers);
} else {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_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<ServerParams> = 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(&param.addr).into_iter())
.flat_map(|params| params.expand_hostnames(&param_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, &param, &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, &param, &mut smtp).await.is_err() {
bail!("SMTP autoconfig did not succeed");
if try_imap_one_param(ctx, &param.imap, &param.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, &param.smtp, &param.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<LoginParam> {
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<Vec<ServerParams>> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, &param).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<LoginParamNew> {
) -> Option<Vec<ServerParams>> {
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<LoginParamNew> {
fn get_offline_autoconfig(context: &Context, addr: &str) -> Option<Vec<ServerParams>> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param.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<LoginParam> {
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<LoginParam> {
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() + &param_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() + &param_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<LoginParam> {
// 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<LoginParam> {
if param.imap.user.is_empty() {
param.imap.user = param.addr.clone();
if let Err(e) = try_imap_one_param(context, &param, 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, &param, imap).await?;
} else {
return Err(e);
}
}
Ok(param)
} else {
try_imap_one_param(context, &param, 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,
&param.imap,
&param.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<LoginParam> {
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() + &param_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() + &param_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<LoginParam> {
// 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<LoginParam> {
if param.smtp.user.is_empty() {
param.smtp.user = param.addr.clone();
if let Err(e) = try_smtp_one_param(context, &param, 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, &param, smtp).await?;
} else {
return Err(e);
}
}
Ok(param)
} else {
try_smtp_one_param(context, &param, 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,
&param.smtp,
&param.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, &params).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, &params).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());
}
}

View File

@@ -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<ServerParams> {
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<ServerParams> {
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<ServerParams> {
// 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
}
}

View File

@@ -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)]

View File

@@ -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<ServerParams>;
pub type SmtpServers = Vec<ServerParams>;
#[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<str>) -> Self {
let prefix = prefix.as_ref();

View File

@@ -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<Oauth2Authorizer>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Vec<ServerParams> {
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::<EmailAddress>() {
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");