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