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::events::{BytesStart, Event};
9
10use super::{Error, ServerParams};
11use crate::context::Context;
12use crate::log::warn;
13use crate::net::read_url;
14use crate::provider::{Protocol, Socket};
15
16#[derive(Debug)]
17struct Server {
18    pub typ: String,
19    pub hostname: String,
20    pub port: u16,
21    pub sockettype: Socket,
22    pub username: String,
23}
24
25#[derive(Debug)]
26struct MozAutoconfigure {
27    pub incoming_servers: Vec<Server>,
28    pub outgoing_servers: Vec<Server>,
29}
30
31#[derive(Debug)]
32enum MozConfigTag {
33    Undefined,
34    Hostname,
35    Port,
36    Sockettype,
37    Username,
38}
39
40impl Default for MozConfigTag {
41    fn default() -> Self {
42        Self::Undefined
43    }
44}
45
46impl FromStr for MozConfigTag {
47    type Err = ();
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        match s.trim().to_lowercase().as_ref() {
51            "hostname" => Ok(MozConfigTag::Hostname),
52            "port" => Ok(MozConfigTag::Port),
53            "sockettype" => Ok(MozConfigTag::Sockettype),
54            "username" => Ok(MozConfigTag::Username),
55            _ => Err(()),
56        }
57    }
58}
59
60/// Parses a single IncomingServer or OutgoingServer section.
61fn parse_server<B: BufRead>(
62    reader: &mut quick_xml::Reader<B>,
63    server_event: &BytesStart,
64) -> Result<Option<Server>, quick_xml::Error> {
65    let end_tag = String::from_utf8_lossy(server_event.name().as_ref())
66        .trim()
67        .to_lowercase();
68
69    let typ = server_event
70        .attributes()
71        .find_map(|attr| {
72            attr.ok().filter(|a| {
73                String::from_utf8_lossy(a.key.as_ref())
74                    .trim()
75                    .eq_ignore_ascii_case("type")
76            })
77        })
78        .map(|typ| {
79            typ.decode_and_unescape_value(reader.decoder())
80                .unwrap_or_default()
81                .to_lowercase()
82        })
83        .unwrap_or_default();
84
85    let mut hostname = None;
86    let mut port = None;
87    let mut sockettype = Socket::Automatic;
88    let mut username = None;
89
90    let mut tag_config = MozConfigTag::Undefined;
91    let mut buf = Vec::new();
92    loop {
93        match reader.read_event_into(&mut buf)? {
94            Event::Start(ref event) => {
95                tag_config = String::from_utf8_lossy(event.name().as_ref())
96                    .parse()
97                    .unwrap_or_default();
98            }
99            Event::End(ref event) => {
100                let tag = String::from_utf8_lossy(event.name().as_ref())
101                    .trim()
102                    .to_lowercase();
103
104                if tag == end_tag {
105                    break;
106                }
107            }
108            Event::Text(ref event) => {
109                let val = event.unescape().unwrap_or_default().trim().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                    if let Some(outgoing_server) = parse_server(reader, event)? {
164                        outgoing_servers.push(outgoing_server);
165                    }
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) -> Result<Vec<ServerParams>, Error> {
258    let xml_raw = read_url(context, url).await?;
259
260    let res = parse_serverparams(addr, &xml_raw);
261    if let Err(err) = &res {
262        warn!(
263            context,
264            "Failed to parse Thunderbird autoconfiguration XML: {}", err
265        );
266    }
267    res
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_parse_outlook_autoconfig() {
276        let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
277        let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
278        assert_eq!(res[0].protocol, Protocol::Imap);
279        assert_eq!(res[0].hostname, "outlook.office365.com");
280        assert_eq!(res[0].port, 993);
281        assert_eq!(res[1].protocol, Protocol::Smtp);
282        assert_eq!(res[1].hostname, "smtp.office365.com");
283        assert_eq!(res[1].port, 587);
284    }
285
286    #[test]
287    fn test_parse_lakenet_autoconfig() {
288        let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
289        let res =
290            parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
291
292        assert_eq!(res.incoming_servers.len(), 4);
293
294        assert_eq!(res.incoming_servers[0].typ, "imap");
295        assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
296        assert_eq!(res.incoming_servers[0].port, 993);
297        assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
298        assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
299
300        assert_eq!(res.incoming_servers[1].typ, "imap");
301        assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
302        assert_eq!(res.incoming_servers[1].port, 143);
303        assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
304        assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
305
306        assert_eq!(res.incoming_servers[2].typ, "pop3");
307        assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
308        assert_eq!(res.incoming_servers[2].port, 995);
309        assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
310        assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
311
312        assert_eq!(res.incoming_servers[3].typ, "pop3");
313        assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
314        assert_eq!(res.incoming_servers[3].port, 110);
315        assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
316        assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
317
318        assert_eq!(res.outgoing_servers.len(), 1);
319
320        assert_eq!(res.outgoing_servers[0].typ, "smtp");
321        assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
322        assert_eq!(res.outgoing_servers[0].port, 587);
323        assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
324        assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
325    }
326}