//! # Thunderbird's Autoconfiguration implementation //! //! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */ use failure::Fail; use quick_xml; use quick_xml::events::{BytesEnd, BytesStart, BytesText}; use crate::constants::*; use crate::context::Context; use crate::login_param::LoginParam; use super::read_url::read_url; #[derive(Debug, Fail)] pub enum Error { #[fail(display = "Invalid email address: {:?}", _0)] InvalidEmailAddress(String), #[fail(display = "XML error at position {}", position)] InvalidXml { position: usize, #[cause] error: quick_xml::Error, }, #[fail(display = "Bad or incomplete autoconfig")] IncompleteAutoconfig(LoginParam), #[fail(display = "Failed to get URL {}", _0)] ReadUrlError(#[cause] super::read_url::Error), } pub type Result = std::result::Result; impl From for Error { fn from(err: super::read_url::Error) -> Error { Error::ReadUrlError(err) } } #[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, } #[derive(Debug, PartialEq)] enum MozServer { Undefined, Imap, Smtp, } #[derive(Debug)] enum MozConfigTag { Undefined, Hostname, Port, Sockettype, 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); // Split address into local part and domain part. let p = in_emailaddr .find('@') .ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?; let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p); let in_emaildomain = &in_emaildomain[1..]; 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 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, _ => (), } buf.clear(); } if moz_ac.out.mail_server.is_empty() || moz_ac.out.mail_port == 0 || moz_ac.out.send_server.is_empty() || moz_ac.out.send_port == 0 { Err(Error::IncompleteAutoconfig(moz_ac.out)) } else { Ok(moz_ac.out) } } pub fn moz_autoconfigure( context: &Context, url: &str, param_in: &LoginParam, ) -> Result { let xml_raw = read_url(context, url)?; let res = parse_xml(¶m_in.addr, &xml_raw); if let Err(err) = &res { warn!( context, "Failed to parse Thunderbird autoconfiguration XML: {}", err ); } 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.mail_server = val, MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(), MozConfigTag::Username => moz_ac.out.mail_user = val, MozConfigTag::Sockettype => { let val_lower = val.to_lowercase(); if val_lower == "ssl" { moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32 } if val_lower == "starttls" { moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32 } if val_lower == "plain" { moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32 } } _ => {} }, MozServer::Smtp => match moz_ac.tag_config { MozConfigTag::Hostname => moz_ac.out.send_server = val, MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(), MozConfigTag::Username => moz_ac.out.send_user = val, MozConfigTag::Sockettype => { let val_lower = val.to_lowercase(); if val_lower == "ssl" { moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32 } if val_lower == "starttls" { moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32 } if val_lower == "plain" { moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32 } } _ => {} }, 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"); assert_eq!(res.mail_server, "outlook.office365.com"); assert_eq!(res.mail_port, 993); assert_eq!(res.send_server, "smtp.office365.com"); assert_eq!(res.send_port, 587); } }