From c19e35b68d40cc1b05c2e607d20733889f4f4cf1 Mon Sep 17 00:00:00 2001 From: Alexander Krotov Date: Sat, 22 Aug 2020 23:31:26 +0300 Subject: [PATCH] Parse multiple servers in Mozilla autoconfig Co-Authored-By: Simon Laux --- src/configure/auto_mozilla.rs | 508 ++++++++++++++------------- test-data/autoconfig/lakenet.ch.xml | 59 ++++ test-data/autoconfig/outlook.com.xml | 71 ++++ 3 files changed, 386 insertions(+), 252 deletions(-) create mode 100644 test-data/autoconfig/lakenet.ch.xml create mode 100644 test-data/autoconfig/outlook.com.xml diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index 887f4e89d..703ed5380 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -1,7 +1,9 @@ //! # Thunderbird's Autoconfiguration implementation //! -//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */ -use quick_xml::events::{BytesEnd, BytesStart, BytesText}; +//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration +use quick_xml::events::{BytesStart, Event}; + +use std::io::BufRead; use crate::context::Context; use crate::login_param::LoginParam; @@ -11,22 +13,18 @@ use super::read_url::read_url; use super::Error; #[derive(Debug)] -struct MozAutoconfigure<'a> { - pub in_emailaddr: &'a str, - pub in_emaildomain: &'a str, - pub in_emaillocalpart: &'a str, - pub out: LoginParam, - pub out_imap_set: bool, - pub out_smtp_set: bool, - pub tag_server: MozServer, - pub tag_config: MozConfigTag, +struct Server { + pub typ: String, + pub hostname: String, + pub port: u16, + pub sockettype: Socket, + pub username: String, } -#[derive(Debug, PartialEq)] -enum MozServer { - Undefined, - Imap, - Smtp, +#[derive(Debug)] +struct MozAutoconfigure { + pub incoming_servers: Vec, + pub outgoing_servers: Vec, } #[derive(Debug)] @@ -38,10 +36,139 @@ enum MozConfigTag { Username, } -fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result { - let mut reader = quick_xml::Reader::from_str(xml_raw); - reader.trim_text(true); +/// Parses a single IncomingServer or OutgoingServer section. +fn parse_server( + reader: &mut quick_xml::Reader, + server_event: &BytesStart, +) -> Result, quick_xml::Error> { + let end_tag = String::from_utf8_lossy(server_event.name()) + .trim() + .to_lowercase(); + let typ = server_event + .attributes() + .find(|attr| { + attr.as_ref() + .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type") + .unwrap_or_default() + }) + .map(|typ| { + typ.unwrap() + .unescape_and_decode_value(reader) + .unwrap_or_default() + .to_lowercase() + }) + .unwrap_or_default(); + + let mut hostname = None; + let mut port = None; + let mut sockettype = Socket::Automatic; + let mut username = None; + + let mut tag_config = MozConfigTag::Undefined; + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf)? { + Event::Start(ref event) => { + let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + + if tag == "hostname" { + tag_config = MozConfigTag::Hostname; + } else if tag == "port" { + tag_config = MozConfigTag::Port; + } else if tag == "sockettype" { + tag_config = MozConfigTag::Sockettype; + } else if tag == "username" { + tag_config = MozConfigTag::Username; + } + } + Event::End(ref event) => { + let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + + if tag == end_tag { + break; + } + } + Event::Text(ref event) => { + let val = event + .unescape_and_decode(reader) + .unwrap_or_default() + .trim() + .to_owned(); + + match tag_config { + MozConfigTag::Hostname => hostname = Some(val), + MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()), + MozConfigTag::Username => username = Some(val), + MozConfigTag::Sockettype => { + let val_lower = val.to_lowercase(); + if val_lower == "ssl" { + sockettype = Socket::SSL; + } + if val_lower == "starttls" { + sockettype = Socket::STARTTLS; + } + if val_lower == "plain" { + sockettype = Socket::Plain; + } + } + _ => {} + } + } + Event::Eof => break, + _ => (), + } + } + + if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) { + Ok(Some(Server { + typ, + hostname, + port, + sockettype, + username, + })) + } else { + Ok(None) + } +} + +fn parse_xml_reader( + reader: &mut quick_xml::Reader, +) -> Result { + let mut incoming_servers = Vec::new(); + let mut outgoing_servers = Vec::new(); + + let mut buf = Vec::new(); + loop { + match reader.read_event(&mut buf)? { + Event::Start(ref event) => { + let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + + if tag == "incomingserver" { + if let Some(incoming_server) = parse_server(reader, event)? { + incoming_servers.push(incoming_server); + } + } else if tag == "outgoingserver" { + if let Some(outgoing_server) = parse_server(reader, event)? { + outgoing_servers.push(outgoing_server); + } + } + } + Event::Eof => break, + _ => (), + } + buf.clear(); + } + + Ok(MozAutoconfigure { + incoming_servers, + outgoing_servers, + }) +} + +/// Parses XML and fills in address and domain placeholders. +fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result { // Split address into local part and domain part. let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect(); let (in_emaillocalpart, in_emaildomain) = match &parts[..] { @@ -49,48 +176,79 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result { _ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())), }; - let mut moz_ac = MozAutoconfigure { - in_emailaddr, - in_emaildomain, - in_emaillocalpart, - out: LoginParam::new(), - out_imap_set: false, - out_smtp_set: false, - tag_server: MozServer::Undefined, - tag_config: MozConfigTag::Undefined, + let mut reader = quick_xml::Reader::from_str(xml_raw); + reader.trim_text(true); + + let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml { + position: reader.buffer_position(), + error, + })?; + + let fill_placeholders = |val: &str| -> String { + val.replace("%EMAILADDRESS%", in_emailaddr) + .replace("%EMAILLOCALPART%", in_emaillocalpart) + .replace("%EMAILDOMAIN%", in_emaildomain) }; - let mut buf = Vec::new(); - 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) => { - moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader) - } - quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac), - quick_xml::events::Event::Text(ref e) => { - moz_autoconfigure_text_cb(e, &mut moz_ac, &reader) - } - quick_xml::events::Event::Eof => break, - _ => (), + let fill_server_placeholders = |server: Server| -> Server { + Server { + typ: server.typ, + hostname: fill_placeholders(&server.hostname), + port: server.port, + sockettype: server.sockettype, + username: fill_placeholders(&server.username), } - buf.clear(); + }; + + Ok(MozAutoconfigure { + incoming_servers: moz_ac + .incoming_servers + .into_iter() + .map(fill_server_placeholders) + .collect(), + outgoing_servers: moz_ac + .outgoing_servers + .into_iter() + .map(fill_server_placeholders) + .collect(), + }) +} + +/// Parses XML into `LoginParam` structure. +fn parse_loginparam(in_emailaddr: &str, xml_raw: &str) -> Result { + let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?; + + let mut login_param = LoginParam::new(); + if let Some(imap_server) = 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 moz_ac.out.imap.server.is_empty() - || moz_ac.out.imap.port == 0 - || moz_ac.out.smtp.server.is_empty() - || moz_ac.out.smtp.port == 0 + if let Some(smtp_server) = moz_ac + .outgoing_servers + .into_iter() + .find(|outgoing_server| outgoing_server.typ == "smtp") { - Err(Error::IncompleteAutoconfig(moz_ac.out)) + 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(moz_ac.out) + Ok(login_param) } } @@ -101,7 +259,7 @@ pub async fn moz_autoconfigure( ) -> Result { let xml_raw = read_url(context, url).await?; - let res = parse_xml(¶m_in.addr, &xml_raw); + let res = parse_loginparam(¶m_in.addr, &xml_raw); if let Err(err) = &res { warn!( context, @@ -111,212 +269,58 @@ pub async fn moz_autoconfigure( res } -fn moz_autoconfigure_text_cb( - event: &BytesText, - moz_ac: &mut MozAutoconfigure, - reader: &quick_xml::Reader, -) { - let val = event.unescape_and_decode(reader).unwrap_or_default(); - - let addr = moz_ac.in_emailaddr; - let email_local = moz_ac.in_emaillocalpart; - let email_domain = moz_ac.in_emaildomain; - - let val = val - .trim() - .replace("%EMAILADDRESS%", addr) - .replace("%EMAILLOCALPART%", email_local) - .replace("%EMAILDOMAIN%", email_domain); - - match moz_ac.tag_server { - MozServer::Imap => match moz_ac.tag_config { - MozConfigTag::Hostname => moz_ac.out.imap.server = val, - MozConfigTag::Port => moz_ac.out.imap.port = val.parse().unwrap_or_default(), - MozConfigTag::Username => moz_ac.out.imap.user = val, - MozConfigTag::Sockettype => { - let val_lower = val.to_lowercase(); - if val_lower == "ssl" { - moz_ac.out.imap.security = Socket::SSL; - } - if val_lower == "starttls" { - moz_ac.out.imap.security = Socket::STARTTLS; - } - if val_lower == "plain" { - moz_ac.out.imap.security = Socket::Plain; - } - } - _ => {} - }, - MozServer::Smtp => match moz_ac.tag_config { - MozConfigTag::Hostname => moz_ac.out.smtp.server = val, - MozConfigTag::Port => moz_ac.out.smtp.port = val.parse().unwrap_or_default(), - MozConfigTag::Username => moz_ac.out.smtp.user = val, - MozConfigTag::Sockettype => { - let val_lower = val.to_lowercase(); - if val_lower == "ssl" { - moz_ac.out.smtp.security = Socket::SSL; - } - if val_lower == "starttls" { - moz_ac.out.smtp.security = Socket::STARTTLS; - } - if val_lower == "plain" { - moz_ac.out.smtp.security = Socket::Plain; - } - } - _ => {} - }, - MozServer::Undefined => {} - } -} - -fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); - - if tag == "incomingserver" { - if moz_ac.tag_server == MozServer::Imap { - moz_ac.out_imap_set = true; - } - moz_ac.tag_server = MozServer::Undefined; - moz_ac.tag_config = MozConfigTag::Undefined; - } else if tag == "outgoingserver" { - if moz_ac.tag_server == MozServer::Smtp { - moz_ac.out_smtp_set = true; - } - moz_ac.tag_server = MozServer::Undefined; - moz_ac.tag_config = MozConfigTag::Undefined; - } else { - moz_ac.tag_config = MozConfigTag::Undefined; - } -} - -fn moz_autoconfigure_starttag_cb( - event: &BytesStart, - moz_ac: &mut MozAutoconfigure, - reader: &quick_xml::Reader, -) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); - - if tag == "incomingserver" { - moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| { - attr.as_ref() - .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type") - .unwrap_or_default() - }) { - let typ = typ - .unwrap() - .unescape_and_decode_value(reader) - .unwrap_or_default() - .to_lowercase(); - - if typ == "imap" && !moz_ac.out_imap_set { - MozServer::Imap - } else { - MozServer::Undefined - } - } else { - MozServer::Undefined - }; - moz_ac.tag_config = MozConfigTag::Undefined; - } else if tag == "outgoingserver" { - moz_ac.tag_server = if !moz_ac.out_smtp_set { - MozServer::Smtp - } else { - MozServer::Undefined - }; - moz_ac.tag_config = MozConfigTag::Undefined; - } else if tag == "hostname" { - moz_ac.tag_config = MozConfigTag::Hostname; - } else if tag == "port" { - moz_ac.tag_config = MozConfigTag::Port; - } else if tag == "sockettype" { - moz_ac.tag_config = MozConfigTag::Sockettype; - } else if tag == "username" { - moz_ac.tag_config = MozConfigTag::Username; - } -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_outlook_autoconfig() { - // Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11 - let xml_raw = -" - - hotmail.com - hotmail.co.uk - hotmail.co.jp - hotmail.com.br - hotmail.de - hotmail.fr - hotmail.it - hotmail.es - live.com - live.co.uk - live.co.jp - live.de - live.fr - live.it - live.jp - msn.com - outlook.com - Outlook.com (Microsoft) - Outlook - - outlook.office365.com - 443 - %EMAILADDRESS% - SSL - OAuth2 - https://outlook.office365.com/owa/ - https://outlook.office365.com/ews/exchange.asmx - true - - - outlook.office365.com - 993 - SSL - password-cleartext - %EMAILADDRESS% - - - outlook.office365.com - 995 - SSL - password-cleartext - %EMAILADDRESS% - - true - - - - - smtp.office365.com - 587 - STARTTLS - password-cleartext - %EMAILADDRESS% - - - Set up an email app with Outlook.com - - - - - - %EMAILADDRESS% - - - - - -"; - let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed"); + 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); } + + #[test] + fn test_parse_lakenet_autoconfig() { + let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml"); + let res = + parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed"); + + assert_eq!(res.incoming_servers.len(), 4); + + assert_eq!(res.incoming_servers[0].typ, "imap"); + assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch"); + assert_eq!(res.incoming_servers[0].port, 993); + assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL); + assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch"); + + assert_eq!(res.incoming_servers[1].typ, "imap"); + assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch"); + assert_eq!(res.incoming_servers[1].port, 143); + assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS); + assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch"); + + assert_eq!(res.incoming_servers[2].typ, "pop3"); + assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch"); + assert_eq!(res.incoming_servers[2].port, 995); + assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL); + assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch"); + + assert_eq!(res.incoming_servers[3].typ, "pop3"); + assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch"); + assert_eq!(res.incoming_servers[3].port, 110); + assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS); + assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch"); + + assert_eq!(res.outgoing_servers.len(), 1); + + assert_eq!(res.outgoing_servers[0].typ, "smtp"); + assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch"); + assert_eq!(res.outgoing_servers[0].port, 587); + assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS); + assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch"); + } } diff --git a/test-data/autoconfig/lakenet.ch.xml b/test-data/autoconfig/lakenet.ch.xml new file mode 100644 index 000000000..f60763fde --- /dev/null +++ b/test-data/autoconfig/lakenet.ch.xml @@ -0,0 +1,59 @@ + + + + + %EMAILDOMAIN% + %EMAILDOMAIN% Mail + %EMAILDOMAIN% + + mail.lakenet.ch + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + mail.lakenet.ch + 143 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + mail.lakenet.ch + 995 + SSL + password-cleartext + %EMAILADDRESS% + + + mail.lakenet.ch + 110 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + mail.lakenet.ch + 587 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + Impostazioni per le e-mail LakeNet + Reglages pour le courriel e-mail LakeNet + Settings for LakeNet's e-mail accounts + + + + + + %EMAILADDRESS% + + + + + + + diff --git a/test-data/autoconfig/outlook.com.xml b/test-data/autoconfig/outlook.com.xml new file mode 100644 index 000000000..49040f710 --- /dev/null +++ b/test-data/autoconfig/outlook.com.xml @@ -0,0 +1,71 @@ + + + + hotmail.com + hotmail.co.uk + hotmail.co.jp + hotmail.com.br + hotmail.de + hotmail.fr + hotmail.it + hotmail.es + live.com + live.co.uk + live.co.jp + live.de + live.fr + live.it + live.jp + msn.com + outlook.com + Outlook.com (Microsoft) + Outlook + + outlook.office365.com + 443 + %EMAILADDRESS% + SSL + OAuth2 + https://outlook.office365.com/owa/ + https://outlook.office365.com/ews/exchange.asmx + true + + + outlook.office365.com + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + outlook.office365.com + 995 + SSL + password-cleartext + %EMAILADDRESS% + + true + + + + + smtp.office365.com + 587 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + Set up an email app with Outlook.com + + + + + + %EMAILADDRESS% + + + + + +";