1pub 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
44pub type Result<T> = std::result::Result<T, Error>;
46
47#[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
74pub 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
93async 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
129async fn from_plain_main_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
132 from_main_isp(http, "http", addr).await
133}
134
135async fn from_secure_main_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
138 from_main_isp(http, "https", addr).await
139}
140
141async 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
156async fn from_plain_alt_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
159 from_alt_isp(http, "http", addr).await
160}
161
162async fn from_secure_alt_isp(http: &HttpClient, addr: &EmailAddress) -> Result<AutoConfig> {
165 from_alt_isp(http, "https", addr).await
166}
167
168async 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
182async 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
196async 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
222async 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
244async 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
256async 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, }),
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
354pub 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 !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}