mailrs_dkim/resolver.rs
1//! DNS resolver trait for DKIM public-key TXT lookups.
2//!
3//! DKIM verifiers need exactly one DNS query type: TXT at
4//! `<selector>._domainkey.<domain>`. We define a minimal trait so
5//! callers plug in their own DNS layer.
6
7use async_trait::async_trait;
8
9use crate::error::DkimError;
10
11/// Minimal DNS interface — DKIM only needs TXT lookups.
12///
13/// Implementors map NXDOMAIN to `Ok(vec![])` (the caller maps that
14/// to [`DkimResult::PermError`] per RFC 6376 §6.1.2). Reserve
15/// `Err(DkimError::DnsTempError)` for actual lookup failures.
16#[async_trait]
17pub trait DkimResolver: Send + Sync {
18 /// TXT records for `domain`. For DKIM, the caller passes
19 /// `<selector>._domainkey.<signing-domain>`.
20 async fn lookup_txt(&self, domain: &str) -> Result<Vec<String>, DkimError>;
21}
22
23/// Ready-made [`DkimResolver`] over `hickory_resolver::TokioResolver`.
24/// Enabled by the default `hickory` feature.
25#[cfg(feature = "hickory")]
26pub mod hickory {
27 use super::*;
28 use hickory_resolver::proto::rr::RData;
29 use hickory_resolver::TokioResolver;
30
31 /// Wrap a `TokioResolver` for use as a [`DkimResolver`].
32 pub struct HickoryDkimResolver {
33 inner: TokioResolver,
34 }
35
36 impl HickoryDkimResolver {
37 /// Construct from an existing `TokioResolver`.
38 pub fn new(resolver: TokioResolver) -> Self {
39 Self { inner: resolver }
40 }
41 }
42
43 /// hickory signals "no records found" via its error message; the
44 /// safest cross-version check is the error text.
45 fn is_no_records<E: std::fmt::Display>(e: &E) -> bool {
46 let s = e.to_string();
47 s.contains("no record")
48 || s.contains("NXDOMAIN")
49 || s.contains("no records found")
50 || s.contains("NoRecordsFound")
51 }
52
53 #[async_trait]
54 impl DkimResolver for HickoryDkimResolver {
55 async fn lookup_txt(&self, domain: &str) -> Result<Vec<String>, DkimError> {
56 match self.inner.txt_lookup(domain).await {
57 Ok(resp) => {
58 let mut out = Vec::new();
59 for record in resp.answers() {
60 if let RData::TXT(txt) = &record.data {
61 out.push(txt.to_string());
62 }
63 }
64 Ok(out)
65 }
66 Err(e) if is_no_records(&e) => Ok(Vec::new()),
67 Err(e) => Err(DkimError::DnsTempError(e.to_string())),
68 }
69 }
70 }
71}