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