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);