Skip to main content

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}