Skip to main content

deltachat/configure/
auto_mozilla.rs

1//! # Thunderbird's Autoconfiguration implementation
2//!
3//! RFC draft: <https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html>
4//! Archived original documentation: <https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
5use std::io::BufRead;
6use std::str::FromStr;
7
8use quick_xml::XmlVersion;
9use quick_xml::events::{BytesStart, Event};
10
11use super::{Error, ServerParams};
12use crate::context::Context;
13use crate::log::warn;
14use crate::net::read_url_with_tls;
15use crate::provider::{Protocol, Socket};
16
17#[derive(Debug)]
18struct Server {
19    pub typ: String,
20    pub hostname: String,
21    pub port: u16,
22    pub sockettype: Socket,
23    pub username: String,
24}
25
26#[derive(Debug)]
27struct MozAutoconfigure {
28    pub incoming_servers: Vec<Server>,
29    pub outgoing_servers: Vec<Server>,
30}
31
32#[derive(Debug, Default)]
33enum MozConfigTag {
34    #[default]
35    Undefined,
36    Hostname,
37    Port,
38    Sockettype,
39    Username,
40}
41
42impl FromStr for MozConfigTag {
43    type Err = ();
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        match s.trim().to_lowercase().as_ref() {
47            "hostname" => Ok(MozConfigTag::Hostname),
48            "port" => Ok(MozConfigTag::Port),
49            "sockettype" => Ok(MozConfigTag::Sockettype),
50            "username" => Ok(MozConfigTag::Username),
51            _ => Err(()),
52        }
53    }
54}
55
56/// Parses a single IncomingServer or OutgoingServer section.
57fn parse_server<B: BufRead>(
58    reader: &mut quick_xml::Reader<B>,
59    server_event: &BytesStart,
60) -> Result<Option<Server>, quick_xml::Error> {
61    let end_tag = String::from_utf8_lossy(server_event.name().as_ref())
62        .trim()
63        .to_lowercase();
64
65    let typ = server_event
66        .attributes()
67        .find_map(|attr| {
68            attr.ok().filter(|a| {
69                String::from_utf8_lossy(a.key.as_ref())
70                    .trim()
71                    .eq_ignore_ascii_case("type")
72            })
73        })
74        .map(|typ| {
75            typ.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
76                .unwrap_or_default()
77                .to_lowercase()
78        })
79        .unwrap_or_default();
80
81    let mut hostname = None;
82    let mut port = None;
83    let mut sockettype = Socket::Automatic;
84    let mut username = None;
85
86    let mut tag_config = MozConfigTag::Undefined;
87    let mut buf = Vec::new();
88    loop {
89        match reader.read_event_into(&mut buf)? {
90            Event::Start(ref event) => {
91                tag_config = String::from_utf8_lossy(event.name().as_ref())
92                    .parse()
93                    .unwrap_or_default();
94            }
95            Event::End(ref event) => {
96                let tag = String::from_utf8_lossy(event.name().as_ref())
97                    .trim()
98                    .to_lowercase();
99
100                if tag == end_tag {
101                    break;
102                }
103            }
104            Event::Text(ref event) => {
105                let val = event
106                    .xml_content(XmlVersion::Implicit1_0)
107                    .unwrap_or_default()
108                    .trim()
109                    .to_owned();
110
111                match tag_config {
112                    MozConfigTag::Hostname => hostname = Some(val),
113                    MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()),
114                    MozConfigTag::Username => username = Some(val),
115                    MozConfigTag::Sockettype => {
116                        sockettype = match val.to_lowercase().as_ref() {
117                            "ssl" => Socket::Ssl,
118                            "starttls" => Socket::Starttls,
119                            "plain" => Socket::Plain,
120                            _ => Socket::Automatic,
121                        }
122                    }
123                    _ => {}
124                }
125            }
126            Event::Eof => break,
127            _ => (),
128        }
129    }
130
131    if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) {
132        Ok(Some(Server {
133            typ,
134            hostname,
135            port,
136            sockettype,
137            username,
138        }))
139    } else {
140        Ok(None)
141    }
142}
143
144fn parse_xml_reader<B: BufRead>(
145    reader: &mut quick_xml::Reader<B>,
146) -> Result<MozAutoconfigure, quick_xml::Error> {
147    let mut incoming_servers = Vec::new();
148    let mut outgoing_servers = Vec::new();
149
150    let mut buf = Vec::new();
151    loop {
152        match reader.read_event_into(&mut buf)? {
153            Event::Start(ref event) => {
154                let tag = String::from_utf8_lossy(event.name().as_ref())
155                    .trim()
156                    .to_lowercase();
157
158                if tag == "incomingserver" {
159                    if let Some(incoming_server) = parse_server(reader, event)? {
160                        incoming_servers.push(incoming_server);
161                    }
162                } else if tag == "outgoingserver"
163                    && let Some(outgoing_server) = parse_server(reader, event)?
164                {
165                    outgoing_servers.push(outgoing_server);
166                }
167            }
168            Event::Eof => break,
169            _ => (),
170        }
171        buf.clear();
172    }
173
174    Ok(MozAutoconfigure {
175        incoming_servers,
176        outgoing_servers,
177    })
178}
179
180/// Parses XML and fills in address and domain placeholders.
181fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
182    // Split address into local part and domain part.
183    let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
184    let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
185        [domain, local] => (local, domain),
186        _ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
187    };
188
189    let mut reader = quick_xml::Reader::from_str(xml_raw);
190    reader.config_mut().trim_text(true);
191
192    let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
193        position: reader.buffer_position(),
194        error,
195    })?;
196
197    let fill_placeholders = |val: &str| -> String {
198        val.replace("%EMAILADDRESS%", in_emailaddr)
199            .replace("%EMAILLOCALPART%", in_emaillocalpart)
200            .replace("%EMAILDOMAIN%", in_emaildomain)
201    };
202
203    let fill_server_placeholders = |server: Server| -> Server {
204        Server {
205            typ: server.typ,
206            hostname: fill_placeholders(&server.hostname),
207            port: server.port,
208            sockettype: server.sockettype,
209            username: fill_placeholders(&server.username),
210        }
211    };
212
213    Ok(MozAutoconfigure {
214        incoming_servers: moz_ac
215            .incoming_servers
216            .into_iter()
217            .map(fill_server_placeholders)
218            .collect(),
219        outgoing_servers: moz_ac
220            .outgoing_servers
221            .into_iter()
222            .map(fill_server_placeholders)
223            .collect(),
224    })
225}
226
227/// Parses XML into `ServerParams` vector.
228fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, Error> {
229    let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?;
230
231    let res = moz_ac
232        .incoming_servers
233        .into_iter()
234        .chain(moz_ac.outgoing_servers)
235        .filter_map(|server| {
236            let protocol = match server.typ.as_ref() {
237                "imap" => Some(Protocol::Imap),
238                "smtp" => Some(Protocol::Smtp),
239                _ => None,
240            };
241            Some(ServerParams {
242                protocol: protocol?,
243                socket: server.sockettype,
244                hostname: server.hostname,
245                port: server.port,
246                username: server.username,
247            })
248        })
249        .collect();
250    Ok(res)
251}
252
253pub(crate) async fn moz_autoconfigure(
254    context: &Context,
255    url: &str,
256    addr: &str,
257    accept_invalid_certificates: bool,
258) -> Result<Vec<ServerParams>, Error> {
259    let xml_raw = read_url_with_tls(context, url, !accept_invalid_certificates).await?;
260
261    let res = parse_serverparams(addr, &xml_raw);
262    if let Err(err) = &res {
263        warn!(
264            context,
265            "Failed to parse Thunderbird autoconfiguration XML: {}", err
266        );
267    }
268    res
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_parse_outlook_autoconfig() {
277        let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
278        let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
279        assert_eq!(res[0].protocol, Protocol::Imap);
280        assert_eq!(res[0].hostname, "outlook.office365.com");
281        assert_eq!(res[0].port, 993);
282        assert_eq!(res[1].protocol, Protocol::Smtp);
283        assert_eq!(res[1].hostname, "smtp.office365.com");
284        assert_eq!(res[1].port, 587);
285    }
286
287    #[test]
288    fn test_parse_lakenet_autoconfig() {
289        let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
290        let res =
291            parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
292
293        assert_eq!(res.incoming_servers.len(), 4);
294
295        assert_eq!(res.incoming_servers[0].typ, "imap");
296        assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
297        assert_eq!(res.incoming_servers[0].port, 993);
298        assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
299        assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
300
301        assert_eq!(res.incoming_servers[1].typ, "imap");
302        assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
303        assert_eq!(res.incoming_servers[1].port, 143);
304        assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
305        assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
306
307        assert_eq!(res.incoming_servers[2].typ, "pop3");
308        assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
309        assert_eq!(res.incoming_servers[2].port, 995);
310        assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
311        assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
312
313        assert_eq!(res.incoming_servers[3].typ, "pop3");
314        assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
315        assert_eq!(res.incoming_servers[3].port, 110);
316        assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
317        assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
318
319        assert_eq!(res.outgoing_servers.len(), 1);
320
321        assert_eq!(res.outgoing_servers[0].typ, "smtp");
322        assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
323        assert_eq!(res.outgoing_servers[0].port, 587);
324        assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
325        assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
326    }
327}