Skip to main content

deltachat/configure/
auto_outlook.rs

1//! # Outlook's Autodiscover
2//!
3//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
4//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
5
6use std::io::BufRead;
7
8use quick_xml::XmlVersion;
9use quick_xml::events::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/// Result of parsing a single `Protocol` tag.
18///
19/// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox>
20#[derive(Debug)]
21struct ProtocolTag {
22    /// Server type, such as "IMAP", "SMTP" or "POP3".
23    ///
24    /// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox>
25    pub typ: String,
26
27    /// Server identifier, hostname or IP address for IMAP and SMTP.
28    ///
29    /// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox>
30    pub server: String,
31
32    /// Network port.
33    ///
34    /// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox>
35    pub port: u16,
36
37    /// Whether connection should be secure, "on" or "off", default is "on".
38    ///
39    /// <https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox>
40    pub ssl: bool,
41}
42
43enum ParsingResult {
44    Protocols(Vec<ProtocolTag>),
45
46    /// XML redirect via `RedirectUrl` tag.
47    RedirectUrl(String),
48}
49
50/// Parses a single Protocol section.
51fn parse_protocol<B: BufRead>(
52    reader: &mut quick_xml::Reader<B>,
53) -> Result<Option<ProtocolTag>, quick_xml::Error> {
54    let mut protocol_type = None;
55    let mut protocol_server = None;
56    let mut protocol_port = None;
57    let mut protocol_ssl = true;
58
59    let mut buf = Vec::new();
60
61    let mut current_tag: Option<String> = None;
62    loop {
63        match reader.read_event_into(&mut buf)? {
64            Event::Start(ref event) => {
65                current_tag = Some(
66                    String::from_utf8_lossy(event.name().as_ref())
67                        .trim()
68                        .to_lowercase(),
69                );
70            }
71            Event::End(ref event) => {
72                let tag = String::from_utf8_lossy(event.name().as_ref())
73                    .trim()
74                    .to_lowercase();
75                if tag == "protocol" {
76                    break;
77                }
78                if Some(tag) == current_tag {
79                    current_tag = None;
80                }
81            }
82            Event::Text(ref e) => {
83                let val = e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default();
84
85                if let Some(ref tag) = current_tag {
86                    match tag.as_str() {
87                        "type" => protocol_type = Some(val.trim().to_string()),
88                        "server" => protocol_server = Some(val.trim().to_string()),
89                        "port" => protocol_port = Some(val.trim().parse().unwrap_or_default()),
90                        "ssl" => {
91                            protocol_ssl = match val.trim() {
92                                "on" => true,
93                                "off" => false,
94                                _ => true,
95                            }
96                        }
97                        _ => {}
98                    };
99                }
100            }
101            Event::Eof => break,
102            _ => {}
103        }
104    }
105
106    if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) =
107        (protocol_type, protocol_server, protocol_port)
108    {
109        Ok(Some(ProtocolTag {
110            typ: protocol_type,
111            server: protocol_server,
112            port: protocol_port,
113            ssl: protocol_ssl,
114        }))
115    } else {
116        Ok(None)
117    }
118}
119
120/// Parses `RedirectUrl` tag.
121fn parse_redirecturl<B: BufRead>(
122    reader: &mut quick_xml::Reader<B>,
123) -> Result<String, quick_xml::Error> {
124    let mut buf = Vec::new();
125    match reader.read_event_into(&mut buf)? {
126        Event::Text(ref e) => {
127            let val = e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default();
128            Ok(val.trim().to_string())
129        }
130        _ => Ok("".to_string()),
131    }
132}
133
134fn parse_xml_reader<B: BufRead>(
135    reader: &mut quick_xml::Reader<B>,
136) -> Result<ParsingResult, quick_xml::Error> {
137    let mut protocols = Vec::new();
138
139    let mut buf = Vec::new();
140    loop {
141        match reader.read_event_into(&mut buf)? {
142            Event::Start(ref e) => {
143                let tag = String::from_utf8_lossy(e.name().as_ref())
144                    .trim()
145                    .to_lowercase();
146
147                if tag == "protocol" {
148                    if let Some(protocol) = parse_protocol(reader)? {
149                        protocols.push(protocol);
150                    }
151                } else if tag == "redirecturl" {
152                    let redirecturl = parse_redirecturl(reader)?;
153                    return Ok(ParsingResult::RedirectUrl(redirecturl));
154                }
155            }
156            Event::Eof => break,
157            _ => (),
158        }
159        buf.clear();
160    }
161
162    Ok(ParsingResult::Protocols(protocols))
163}
164
165fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
166    let mut reader = quick_xml::Reader::from_str(xml_raw);
167    reader.config_mut().trim_text(true);
168
169    parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
170        position: reader.buffer_position(),
171        error,
172    })
173}
174
175fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
176    protocols
177        .into_iter()
178        .filter_map(|protocol| {
179            Some(ServerParams {
180                protocol: match protocol.typ.to_lowercase().as_ref() {
181                    "imap" => Some(Protocol::Imap),
182                    "smtp" => Some(Protocol::Smtp),
183                    _ => None,
184                }?,
185                socket: match protocol.ssl {
186                    true => Socket::Automatic,
187                    false => Socket::Plain,
188                },
189                hostname: protocol.server,
190                port: protocol.port,
191                username: String::new(),
192            })
193        })
194        .collect()
195}
196
197pub(crate) async fn outlk_autodiscover(
198    context: &Context,
199    mut url: String,
200    accept_invalid_certificates: bool,
201) -> Result<Vec<ServerParams>, Error> {
202    /* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
203    for _i in 0..10 {
204        let xml_raw = read_url_with_tls(context, &url, !accept_invalid_certificates).await?;
205        let res = parse_xml(&xml_raw);
206        if let Err(err) = &res {
207            warn!(context, "{}", err);
208        }
209        match res? {
210            ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
211            ParsingResult::Protocols(protocols) => {
212                return Ok(protocols_to_serverparams(protocols));
213            }
214        }
215    }
216    Err(Error::Redirection)
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_parse_redirect() {
225        let res = parse_xml("
226<?xml version=\"1.0\" encoding=\"utf-8\"?>
227  <Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">
228    <Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">
229      <Account>
230        <AccountType>email</AccountType>
231        <Action>redirectUrl</Action>
232        <RedirectUrl>https://mail.example.com/autodiscover/autodiscover.xml</RedirectUrl>
233      </Account>
234    </Response>
235  </Autodiscover>
236 ").expect("XML is not parsed successfully");
237        if let ParsingResult::RedirectUrl(url) = res {
238            assert_eq!(
239                url,
240                "https://mail.example.com/autodiscover/autodiscover.xml"
241            );
242        } else {
243            panic!("redirecturl is not found");
244        }
245    }
246
247    #[test]
248    fn test_parse_loginparam() {
249        let res = parse_xml(
250            "\
251<?xml version=\"1.0\" encoding=\"utf-8\"?>
252<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006\">
253  <Response xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\">
254    <Account>
255      <AccountType>email</AccountType>
256      <Action>settings</Action>
257      <Protocol>
258        <Type>IMAP</Type>
259        <Server>example.com</Server>
260        <Port>993</Port>
261        <SSL>on</SSL>
262        <AuthRequired>on</AuthRequired>
263      </Protocol>
264      <Protocol>
265        <Type>SMTP</Type>
266        <Server>smtp.example.com</Server>
267        <Port>25</Port>
268        <SSL>off</SSL>
269        <AuthRequired>on</AuthRequired>
270      </Protocol>
271    </Account>
272  </Response>
273</Autodiscover>",
274        )
275        .expect("XML is not parsed successfully");
276
277        match res {
278            ParsingResult::Protocols(protocols) => {
279                assert_eq!(protocols[0].typ, "IMAP");
280                assert_eq!(protocols[0].server, "example.com");
281                assert_eq!(protocols[0].port, 993);
282                assert_eq!(protocols[0].ssl, true);
283
284                assert_eq!(protocols[1].typ, "SMTP");
285                assert_eq!(protocols[1].server, "smtp.example.com");
286                assert_eq!(protocols[1].port, 25);
287                assert_eq!(protocols[1].ssl, false);
288            }
289            ParsingResult::RedirectUrl(_) => {
290                panic!("RedirectUrl is not expected");
291            }
292        }
293    }
294}