1use 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
20static MAILCONF_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^mailconf=(https://\S+)$").unwrap());
23
24#[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#[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 let priority_cmp = self.priority().cmp(&other.priority());
87
88 if priority_cmp == Ordering::Equal {
89 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
103pub struct DnsClient {
105 resolver: TokioAsyncResolver,
106}
107
108impl DnsClient {
109 pub fn new() -> Self {
111 let resolver = TokioAsyncResolver::tokio(Default::default(), Default::default());
112 Self { resolver }
113 }
114
115 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 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 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 #[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 #[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 #[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}