email/autoconfig/
dns.rs

1//! # Account DNS discovery
2//!
3//! This module contains everything needed to discover account using
4//! DNS records.
5
6use std::{cmp::Ordering, ops::Deref};
7
8use hickory_resolver::{
9    proto::rr::rdata::{MX, SRV},
10    TokioAsyncResolver,
11};
12use http::ureq::http::Uri;
13use once_cell::sync::Lazy;
14use regex::Regex;
15use tracing::{debug, trace};
16
17#[doc(inline)]
18pub use super::{Error, Result};
19
20/// Regular expression used to extract the URI of a mailconf TXT
21/// record.
22static MAILCONF_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^mailconf=(https://\S+)$").unwrap());
23
24/// Sortable wrapper around a MX record.
25///
26/// This wrapper allows MX records to be sorted by preference.
27#[derive(Debug, Clone, Eq, PartialEq)]
28pub struct MxRecord(MX);
29
30impl MxRecord {
31    pub fn new(record: MX) -> Self {
32        Self(record)
33    }
34}
35
36impl Deref for MxRecord {
37    type Target = MX;
38
39    fn deref(&self) -> &Self::Target {
40        &self.0
41    }
42}
43
44impl Ord for MxRecord {
45    fn cmp(&self, other: &Self) -> Ordering {
46        self.preference().cmp(&other.preference())
47    }
48}
49
50impl PartialOrd for MxRecord {
51    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52        Some(self.cmp(other))
53    }
54}
55
56/// Sortable wrapper around a SRV record.
57///
58/// This wrapper allows MX records to be sorted by priority then
59/// weight.
60#[derive(Debug, Clone, Eq, PartialEq)]
61struct SrvRecord(SRV);
62
63impl SrvRecord {
64    pub fn new(record: SRV) -> Self {
65        Self(record)
66    }
67}
68
69impl Deref for SrvRecord {
70    type Target = SRV;
71
72    fn deref(&self) -> &Self::Target {
73        &self.0
74    }
75}
76
77impl From<SrvRecord> for SRV {
78    fn from(val: SrvRecord) -> Self {
79        val.0
80    }
81}
82
83impl Ord for SrvRecord {
84    fn cmp(&self, other: &Self) -> Ordering {
85        // sort by priority in ascending order
86        let priority_cmp = self.priority().cmp(&other.priority());
87
88        if priority_cmp == Ordering::Equal {
89            // sort by weight in descending order
90            other.weight().cmp(&self.weight())
91        } else {
92            priority_cmp
93        }
94    }
95}
96
97impl PartialOrd for SrvRecord {
98    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99        Some(self.cmp(other))
100    }
101}
102
103/// Simple DNS client using the tokio async resolver.
104pub struct DnsClient {
105    resolver: TokioAsyncResolver,
106}
107
108impl DnsClient {
109    /// Create a new DNS client using defaults.
110    pub fn new() -> Self {
111        let resolver = TokioAsyncResolver::tokio(Default::default(), Default::default());
112        Self { resolver }
113    }
114
115    /// Get the first mailconf URI of TXT records from the given
116    /// domain.
117    pub async fn get_mailconf_txt_uri(&self, domain: &str) -> Result<Uri> {
118        let records: Vec<String> = self
119            .resolver
120            .txt_lookup(domain)
121            .await
122            .map_err(Error::LookUpTxtError)?
123            .into_iter()
124            .map(|record| record.to_string())
125            .collect();
126
127        debug!("{domain}: discovered {} TXT record(s)", records.len());
128        trace!("{records:#?}");
129
130        let uri = records
131            .into_iter()
132            .find_map(|record| {
133                MAILCONF_REGEX
134                    .captures(&record)
135                    .and_then(|captures| captures.get(1))
136                    .and_then(|capture| capture.as_str().parse::<Uri>().ok())
137            })
138            .ok_or_else(|| Error::GetMailconfTxtRecordNotFoundError(domain.to_owned()))?;
139
140        debug!("{domain}: best TXT mailconf URI found: {uri}");
141
142        Ok(uri)
143    }
144
145    /// Get the first MX exchange domain from a given domain.
146    pub async fn get_mx_domain(&self, domain: &str) -> Result<String> {
147        let mut records: Vec<MxRecord> = self
148            .resolver
149            .mx_lookup(domain)
150            .await
151            .map_err(Error::LookUpMxError)?
152            .into_iter()
153            .map(MxRecord::new)
154            .collect();
155
156        records.sort();
157
158        debug!("{domain}: discovered {} MX record(s)", records.len());
159        trace!("{records:#?}");
160
161        let record = records
162            .into_iter()
163            .next()
164            .ok_or_else(|| Error::GetMxRecordNotFoundError(domain.to_owned()))?;
165
166        let exchange = record.exchange().trim_to(2).to_string();
167
168        debug!("{domain}: best MX domain found: {exchange}");
169
170        Ok(exchange)
171    }
172
173    /// Get the first SRV record from a given domain and subdomain.
174    pub async fn get_srv(&self, domain: &str, subdomain: &str) -> Result<SRV> {
175        let domain = format!("_{subdomain}._tcp.{domain}");
176
177        let mut records: Vec<SrvRecord> = self
178            .resolver
179            .srv_lookup(&domain)
180            .await
181            .map_err(Error::LookUpSrvError)?
182            .into_iter()
183            .filter(|record| !record.target().is_root())
184            .map(SrvRecord::new)
185            .collect();
186
187        records.sort();
188
189        debug!("{domain}: discovered {} SRV record(s)", records.len());
190        trace!("{records:#?}");
191
192        let record: SRV = records
193            .into_iter()
194            .next()
195            .ok_or_else(|| Error::GetSrvRecordNotFoundError(domain.clone()))?
196            .into();
197
198        debug!("{domain}: best SRV record found: {record}");
199
200        Ok(record)
201    }
202
203    /// Get the first IMAP SRV record from a given domain.
204    #[cfg(feature = "imap")]
205    pub async fn get_imap_srv(&self, domain: &str) -> Result<SRV> {
206        self.get_srv(domain, "imap").await
207    }
208
209    /// Get the first IMAPS SRV record from a given domain.
210    #[cfg(feature = "imap")]
211    pub async fn get_imaps_srv(&self, domain: &str) -> Result<SRV> {
212        self.get_srv(domain, "imaps").await
213    }
214
215    /// Get the first SMTP(S) SRV record from a given domain.
216    #[cfg(feature = "smtp")]
217    pub async fn get_submission_srv(&self, domain: &str) -> Result<SRV> {
218        self.get_srv(domain, "submission").await
219    }
220}
221
222impl Default for DnsClient {
223    fn default() -> Self {
224        Self::new()
225    }
226}