email/autoconfig/
mod.rs

1//! # Account discovery
2//!
3//! This module contains everything needed to discover account
4//! configuration from a simple email address, heavily inspired by the
5//! Thunderbird [Autoconfiguration] standard.
6//!
7//! *NOTE: only IMAP and SMTP configurations can be discovered by this
8//! module.*
9//!
10//! Discovery performs actions in this order:
11//!
12//! - Check ISP databases for example.com
13//!   - Check main ISP <autoconfig.example.com>
14//!   - Check alt ISP <example.com/.well-known>
15//!   - Check Thunderbird ISPDB <autoconfig.thunderbird.net/example.com>
16//! - Check example.com DNS records
17//!   - If example2.com found in example.com MX records
18//!     - Check ISP databases for example2.com
19//!     - Check for mailconf URI in example2.com TXT records
20//!   - Check mailconf URI in example.com TXT records
21//!   - Build autoconfig from imap and submission example.com SRV records
22//!
23//! [Autoconfiguration]: https://udn.realityripple.com/docs/Mozilla/Thunderbird/Autoconfiguration
24
25pub mod config;
26pub mod dns;
27
28use std::str::FromStr;
29
30use email_address::EmailAddress;
31use futures::{future::select_ok, FutureExt};
32use http::{
33    ureq::http::{StatusCode, Uri},
34    Client as HttpClient,
35};
36use thiserror::Error;
37use tracing::{debug, trace};
38
39use self::{
40    config::{AutoConfig, EmailProvider},
41    dns::DnsClient,
42};
43
44/// The global `Result` alias of the module.
45pub type Result<T> = std::result::Result<T, Error>;
46
47/// The global `Error` enum of the module.
48#[derive(Debug, Error)]
49pub enum Error {
50    #[error("cannot create autoconfig HTTP connector")]
51    CreateHttpConnectorError(#[source] std::io::Error),
52    #[error("cannot find any MX record at {0}")]
53    GetMxRecordNotFoundError(String),
54    #[error("cannot find any mailconf TXT record at {0}")]
55    GetMailconfTxtRecordNotFoundError(String),
56    #[error("cannot find any SRV record at {0}")]
57    GetSrvRecordNotFoundError(String),
58    #[error("cannot do txt lookup: {0}")]
59    LookUpTxtError(#[source] hickory_resolver::error::ResolveError),
60    #[error("cannot do mx lookup: {0}")]
61    LookUpMxError(#[source] hickory_resolver::error::ResolveError),
62    #[error("cannot do srv lookup: {0}")]
63    LookUpSrvError(#[source] hickory_resolver::error::ResolveError),
64    #[error("cannot get autoconfig from {0}: {1}")]
65    GetAutoConfigError(String, StatusCode, Uri),
66    #[error("error while getting autoconfig from {1}")]
67    SendGetRequestError(#[source] http::Error, Uri),
68    #[error("cannot decode autoconfig of HTTP response body from {1}")]
69    SerdeXmlFailedForAutoConfig(#[source] serde_xml_rs::Error, Uri),
70    #[error("cannot parse email {0}: {1}")]
71    ParsingEmailAddress(String, #[source] email_address::Error),
72}
73
74/// Discover configuration associated to a given email address using
75/// ISP locations then DNS, as described in the Mozilla [wiki].
76///
77/// [wiki]: https://wiki.mozilla.org/Thunderbird:Autoconfiguration#Implementation
78pub async fn from_addr(addr: impl AsRef<str>) -> Result<AutoConfig> {
79    let addr = EmailAddress::from_str(addr.as_ref())
80        .map_err(|e| Error::ParsingEmailAddress(addr.as_ref().to_string(), e))?;
81    let http = HttpClient::new();
82
83    match from_isps(&http, &addr).await {
84        Ok(config) => Ok(config),
85        Err(err) => {
86            let log = "ISP discovery failed, trying DNS…";
87            debug!(addr = addr.to_string(), ?err, "{log}");
88            from_dns(&http, &addr).await
89        }
90    }
91}
92
93/// Discover configuration associated to a given email address using
94/// different ISP locations, as described in the Mozilla [wiki].
95///
96/// Inspect first main ISP locations, then inspect alternative ISP
97/// locations.
98///
99/// [wiki]: https://wiki.mozilla.org/Thunderbird:Autoconfiguration#Implementation
100async fn from_isps(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
101    let from_main_isps = [
102        from_plain_main_isp(http, addr).boxed(),
103        from_secure_main_isp(http, addr).boxed(),
104    ];
105
106    match select_ok(from_main_isps).await {
107        Ok((config, _)) => Ok(config),
108        Err(err) => {
109            let log = "main ISP discovery failed, trying alternative ISP…";
110            debug!(addr = addr.to_string(), ?err, "{log}");
111
112            let from_alt_isps = [
113                from_plain_alt_isp(http, addr).boxed(),
114                from_secure_alt_isp(http, addr).boxed(),
115            ];
116
117            match select_ok(from_alt_isps).await {
118                Ok((config, _)) => Ok(config),
119                Err(err) => {
120                    let log = "alternative ISP discovery failed, trying ISPDB…";
121                    debug!(addr = addr.to_string(), ?err, "{log}");
122                    from_ispdb(http, addr).await
123                }
124            }
125        }
126    }
127}
128
129/// Discover configuration associated to a given email address using
130/// plain main ISP location (http).
131async fn from_plain_main_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
132    from_main_isp(http, "http", addr).await
133}
134
135/// Discover configuration associated to a given email address using
136/// secure main ISP location (https).
137async fn from_secure_main_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
138    from_main_isp(http, "https", addr).await
139}
140
141/// Discover configuration associated to a given email address using
142/// main ISP location.
143async fn from_main_isp(http: &HttpClient, scheme: &str, addr: &EmailAddress) -> Result<AutoConfig> {
144    let domain = addr.domain().trim_matches('.');
145    let uri_str =
146        format!("{scheme}://autoconfig.{domain}/mail/config-v1.1.xml?emailaddress={addr}");
147    let uri = Uri::from_str(&uri_str).unwrap();
148
149    let config = get_config(http, uri).await?;
150    debug!("successfully discovered config from ISP at {uri_str}");
151    trace!("{config:#?}");
152
153    Ok(config)
154}
155
156/// Discover configuration associated to a given email address using
157/// plain alternative ISP location (http).
158async fn from_plain_alt_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
159    from_alt_isp(http, "http", addr).await
160}
161
162/// Discover configuration associated to a given email address using
163/// secure alternative ISP location (https).
164async fn from_secure_alt_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
165    from_alt_isp(http, "https", addr).await
166}
167
168/// Discover configuration associated to a given email address using
169/// alternative ISP location.
170async fn from_alt_isp(http: &HttpClient, scheme: &str, addr: &EmailAddress) -> Result<AutoConfig> {
171    let domain = addr.domain().trim_matches('.');
172    let uri_str = format!("{scheme}://{domain}/.well-known/autoconfig/mail/config-v1.1.xml");
173    let uri = Uri::from_str(&uri_str).unwrap();
174
175    let config = get_config(http, uri).await?;
176    debug!("successfully discovered config from ISP at {uri_str}");
177    trace!("{config:#?}");
178
179    Ok(config)
180}
181
182/// Discover configuration associated to a given email address using
183/// Thunderbird ISPDB.
184async fn from_ispdb(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
185    let domain = addr.domain().trim_matches('.');
186    let uri_str = format!("https://autoconfig.thunderbird.net/v1.1/{domain}");
187    let uri = Uri::from_str(&uri_str).unwrap();
188
189    let config = get_config(http, uri).await?;
190    debug!("successfully discovered config from ISPDB at {uri_str}");
191    trace!("{config:#?}");
192
193    Ok(config)
194}
195
196/// Discover configuration associated to a given email address using
197/// different DNS records.
198///
199/// Inspect first MX records, then TXT records, and finally SRV
200/// records.
201async fn from_dns(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
202    let domain = addr.domain().trim_matches('.');
203    let dns = DnsClient::new();
204
205    match from_dns_mx(http, &dns, addr).await {
206        Ok(config) => Ok(config),
207        Err(err) => {
208            let addr = addr.to_string();
209            debug!(addr, ?err, "MX discovery failed, trying TXT…");
210            match from_dns_txt(http, &dns, domain).await {
211                Ok(config) => Ok(config),
212                Err(err) => {
213                    let addr = addr.to_string();
214                    debug!(addr, ?err, "TXT discovery failed, trying SRV…");
215                    from_dns_srv(&dns, domain).await
216                }
217            }
218        }
219    }
220}
221
222/// Discover configuration associated to a given email address using
223/// MX DNS records.
224async fn from_dns_mx(
225    http: &HttpClient,
226    dns: &DnsClient,
227    addr: &EmailAddress,
228) -> Result<AutoConfig> {
229    let local_part = addr.local_part();
230    let domain = dns.get_mx_domain(addr.domain()).await?;
231    let domain = domain.trim_matches('.');
232    let addr = EmailAddress::from_str(&format!("{local_part}@{domain}")).unwrap();
233
234    match from_isps(http, &addr).await {
235        Ok(config) => Ok(config),
236        Err(err) => {
237            let addr = addr.to_string();
238            debug!(addr, ?err, "ISP discovery failed, trying TXT…");
239            from_dns_txt(http, dns, domain).await
240        }
241    }
242}
243
244/// Discover configuration associated to a given email address using
245/// TXT DNS records.
246async fn from_dns_txt(http: &HttpClient, dns: &DnsClient, domain: &str) -> Result<AutoConfig> {
247    let uri = dns.get_mailconf_txt_uri(domain).await?;
248
249    let config = get_config(http, uri).await?;
250    debug!("successfully discovered config from {domain} TXT record");
251    trace!("{config:#?}");
252
253    Ok(config)
254}
255
256/// Discover configuration associated to a given email address using
257/// SRV DNS records.
258async fn from_dns_srv(
259    #[allow(unused_variables)] dns: &DnsClient,
260    domain: &str,
261) -> Result<AutoConfig> {
262    #[allow(unused_mut)]
263    let mut config = AutoConfig {
264        version: String::from("1.1"),
265        email_provider: EmailProvider {
266            id: domain.to_owned(),
267            properties: Vec::new(),
268        },
269        oauth2: None,
270    };
271
272    #[cfg(feature = "imap")]
273    if let Ok(record) = dns.get_imap_srv(domain).await {
274        let mut target = record.target().clone();
275        target.set_fqdn(false);
276
277        use self::config::{
278            AuthenticationType, EmailProviderProperty, SecurityType, Server, ServerProperty,
279            ServerType,
280        };
281
282        config
283            .email_provider
284            .properties
285            .push(EmailProviderProperty::IncomingServer(Server {
286                r#type: ServerType::Imap,
287                properties: vec![
288                    ServerProperty::Hostname(target.to_string()),
289                    ServerProperty::Port(record.port()),
290                    ServerProperty::SocketType(SecurityType::Starttls),
291                    ServerProperty::Authentication(AuthenticationType::PasswordCleartext),
292                ],
293            }))
294    }
295
296    #[cfg(feature = "imap")]
297    if let Ok(record) = dns.get_imaps_srv(domain).await {
298        let mut target = record.target().clone();
299        target.set_fqdn(false);
300
301        use self::config::{
302            AuthenticationType, EmailProviderProperty, SecurityType, Server, ServerProperty,
303            ServerType,
304        };
305
306        config
307            .email_provider
308            .properties
309            .push(EmailProviderProperty::IncomingServer(Server {
310                r#type: ServerType::Imap,
311                properties: vec![
312                    ServerProperty::Hostname(target.to_string()),
313                    ServerProperty::Port(record.port()),
314                    ServerProperty::SocketType(SecurityType::Tls),
315                    ServerProperty::Authentication(AuthenticationType::PasswordCleartext),
316                ],
317            }))
318    }
319
320    #[cfg(feature = "smtp")]
321    if let Ok(record) = dns.get_submission_srv(domain).await {
322        let mut target = record.target().clone();
323        target.set_fqdn(false);
324
325        use self::config::{
326            AuthenticationType, EmailProviderProperty, SecurityType, Server, ServerProperty,
327            ServerType,
328        };
329
330        config
331            .email_provider
332            .properties
333            .push(EmailProviderProperty::OutgoingServer(Server {
334                r#type: ServerType::Smtp,
335                properties: vec![
336                    ServerProperty::Hostname(target.to_string()),
337                    ServerProperty::Port(record.port()),
338                    ServerProperty::SocketType(match record.port() {
339                        25 => SecurityType::Plain,
340                        587 => SecurityType::Starttls,
341                        _ => SecurityType::Tls, // including 456
342                    }),
343                    ServerProperty::Authentication(AuthenticationType::PasswordCleartext),
344                ],
345            }))
346    }
347
348    debug!("successfully discovered config from {domain} SRV record");
349    trace!("{config:#?}");
350
351    Ok(config)
352}
353
354/// Send a GET request to the given URI and try to parse response
355/// as autoconfig.
356pub async fn get_config(http: &HttpClient, uri: Uri) -> Result<AutoConfig> {
357    let uri_clone = uri.clone();
358    let res = http
359        .send(move |agent| agent.get(uri_clone).call())
360        .await
361        .map_err(|err| Error::SendGetRequestError(err, uri.clone()))?;
362
363    let status = res.status();
364    let mut body = res.into_body();
365
366    // If we got an error response we return an error
367    if !status.is_success() {
368        let err = match body.read_to_string() {
369            Ok(err) => err,
370            Err(err) => {
371                format!("unparsable error: {err}")
372            }
373        };
374
375        return Err(Error::GetAutoConfigError(err, status, uri.clone()));
376    }
377
378    serde_xml_rs::from_reader(body.as_reader())
379        .map_err(|err| Error::SerdeXmlFailedForAutoConfig(err, uri))
380}