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