1use 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
55fn 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
175fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
177 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
222fn 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}