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

@@ -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());
}
}