Skip to main content

mail_auth/dns/
txt.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! The parsed TXT record type stored in DNS caches.
8
9use crate::{
10    Error,
11    dkim::{AtpsRecord, DkimReportRecord, DomainKey},
12    dmarc::DmarcRecord,
13    mta_sts::{MtaStsRecord, TlsRptRecord},
14    spf::{Macro, SpfRecord},
15};
16use std::sync::Arc;
17
18/// A parsed DNS TXT record, as stored in the TXT [`ResolverCache`] of a
19/// [`DnsCache`].
20///
21/// [`MessageAuthenticator::txt_lookup`](crate::MessageAuthenticator::txt_lookup)
22/// parses the TXT answer into the record type the caller asked for and caches
23/// the outcome as a `TxtRecord`, so a cache hit skips both the query and the
24/// parse. When no TXT record of the requested type parses, the error is
25/// cached as [`TxtRecord::Error`], and so is a negative answer (`NXDOMAIN` or
26/// NODATA) that carries an SOA record; resolver failures are not cached. Each
27/// record type converts into its variant with `From`, which lets cache
28/// implementations pre-populate entries.
29///
30/// [`ResolverCache`]: super::ResolverCache
31/// [`DnsCache`]: super::DnsCache
32#[derive(Clone)]
33pub enum TxtRecord {
34    /// An SPF record (RFC 7208 Section 4.5).
35    Spf(Arc<SpfRecord>),
36    /// The explanation string fetched from the target of an SPF `exp=`
37    /// modifier (RFC 7208 Section 6.2), kept unexpanded.
38    SpfExplanation(Arc<Macro>),
39    /// A DKIM public key record (RFC 6376 Section 3.6.1).
40    DomainKey(Arc<DomainKey>),
41    /// A DKIM failure reporting record (RFC 6651 Section 3).
42    DkimReport(Arc<DkimReportRecord>),
43    /// A DMARC policy record (RFC 9989).
44    Dmarc(Arc<DmarcRecord>),
45    /// A DKIM Authorized Third-Party Signatures record (RFC 6541 Section 4).
46    Atps(Arc<AtpsRecord>),
47    /// An MTA-STS record (RFC 8461 Section 3.1).
48    MtaSts(Arc<MtaStsRecord>),
49    /// An SMTP TLS Reporting record (RFC 8460 Section 3).
50    TlsRpt(Arc<TlsRptRecord>),
51    /// No TXT record at the name parsed as the requested type, or the name
52    /// has no TXT records. Holds the error returned to the caller.
53    Error(Error),
54}
55
56/// Parser for a record type published in DNS TXT records.
57///
58/// Implemented by every record type that
59/// [`MessageAuthenticator::txt_lookup`](crate::MessageAuthenticator::txt_lookup)
60/// can fetch.
61pub trait TxtRecordParser: Sized {
62    /// Parses the contents of one TXT record, with its character strings
63    /// already concatenated.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`Error::Dns`] with [`DnsError::InvalidRecordType`](crate::DnsError::InvalidRecordType)
68    /// when the record is not of this type (for example, a missing or wrong
69    /// version tag), or another [`Error`] when the record is of this type but
70    /// malformed.
71    fn parse(record: &[u8]) -> crate::Result<Self>;
72
73    /// Parses the TXT records found at one name, each with its character
74    /// strings already concatenated. The default implementation returns the
75    /// first record that [`parse`](Self::parse) accepts, or the error of the
76    /// last one.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`Error::Dns`] with [`DnsError::InvalidRecordType`](crate::DnsError::InvalidRecordType)
81    /// when `records` is empty, or the error of the last record otherwise.
82    fn parse_records(records: &[Vec<u8>]) -> crate::Result<Self> {
83        let mut result = Err(Error::Dns(crate::DnsError::InvalidRecordType));
84        for record in records {
85            result = Self::parse(record);
86            if result.is_ok() {
87                break;
88            }
89        }
90        result
91    }
92}
93
94mod private {
95    pub trait Sealed {}
96}
97
98#[doc(hidden)]
99pub trait UnwrapTxtRecord: private::Sealed + Sized {
100    fn unwrap_txt(txt: TxtRecord) -> crate::Result<Arc<Self>>;
101}
102
103impl<T: Into<TxtRecord>> From<crate::Result<T>> for TxtRecord {
104    fn from(v: crate::Result<T>) -> Self {
105        match v {
106            Ok(v) => v.into(),
107            Err(err) => TxtRecord::Error(err),
108        }
109    }
110}
111
112macro_rules! txt_record {
113    ($($record:ty => $variant:ident),+ $(,)?) => {
114        $(
115            impl From<$record> for TxtRecord {
116                fn from(v: $record) -> Self {
117                    TxtRecord::$variant(v.into())
118                }
119            }
120
121            impl private::Sealed for $record {}
122
123            impl UnwrapTxtRecord for $record {
124                fn unwrap_txt(txt: TxtRecord) -> crate::Result<Arc<Self>> {
125                    match txt {
126                        TxtRecord::$variant(a) => Ok(a),
127                        TxtRecord::Error(err) => Err(err),
128                        _ => Err(Error::Io("Invalid record type".to_string())),
129                    }
130                }
131            }
132        )+
133    };
134}
135
136txt_record!(
137    DomainKey => DomainKey,
138    DkimReportRecord => DkimReport,
139    AtpsRecord => Atps,
140    SpfRecord => Spf,
141    Macro => SpfExplanation,
142    DmarcRecord => Dmarc,
143    MtaStsRecord => MtaSts,
144    TlsRptRecord => TlsRpt,
145);