Skip to main content

mail_auth/dmarc/
mod.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//! Domain-based Message Authentication, Reporting, and Conformance (DMARC).
8//!
9//! Implements DMARC policy discovery and evaluation as specified in
10//! [RFC 9989](https://datatracker.ietf.org/doc/html/rfc9989), including the
11//! DNS Tree Walk, Public Suffix Domain (`psd=`) handling and the external
12//! reporting destination check. Reports themselves are covered by
13//! [RFC 9990](https://datatracker.ietf.org/doc/html/rfc9990) (aggregate) and
14//! [RFC 9991](https://datatracker.ietf.org/doc/html/rfc9991) (failure); see
15//! the `report` module. Indirect mail flows are discussed in
16//! [RFC 7960](https://datatracker.ietf.org/doc/html/rfc7960).
17//!
18//! The entry point is
19//! [`MessageAuthenticator::verify_dmarc`](crate::MessageAuthenticator::verify_dmarc),
20//! which takes the results of DKIM (and optionally DKIM2) and SPF
21//! verification in [`DmarcParameters`](verify::DmarcParameters) and returns
22//! a [`DmarcOutput`].
23
24use serde::{Deserialize, Serialize};
25use std::fmt::Display;
26
27/// Verification result types: [`DmarcOutput`] and [`DmarcResult`].
28pub mod output;
29/// Parser for `v=DMARC1` TXT records.
30pub mod parse;
31/// DMARC evaluation (RFC 9989, Section 4.10) and reporting address
32/// authorization.
33pub mod verify;
34
35pub use output::{DmarcOutput, DmarcResult};
36
37/// A DMARC Policy Record (RFC 9989, Section 4.7).
38///
39/// Parsed from a `v=DMARC1` DNS TXT record published at `_dmarc.<domain>`
40/// and found by the DNS Tree Walk of
41/// [`MessageAuthenticator::verify_dmarc`](crate::MessageAuthenticator::verify_dmarc),
42/// which exposes the applied record through [`DmarcOutput::record`]. Each
43/// field is named after its tag.
44#[derive(Debug, Hash, Clone, PartialEq, Eq)]
45pub struct DmarcRecord {
46    /// `adkim=`: DKIM identifier alignment mode (default relaxed).
47    pub adkim: Alignment,
48    /// `aspf=`: SPF identifier alignment mode (default relaxed).
49    pub aspf: Alignment,
50    /// `fo=`: failure reporting options (default `0`, [`FailureOptions::All`]).
51    pub fo: FailureOptions,
52    /// `np=`: policy for non-existent subdomains; defaults to `sp`.
53    pub np: Policy,
54    /// `p=`: policy for the domain itself; [`Policy::Unspecified`] when the
55    /// tag is missing.
56    pub p: Policy,
57    /// `psd=`: whether the domain is a Public Suffix Domain.
58    pub psd: Psd,
59    /// `rua=`: destinations for aggregate reports (RFC 9990).
60    pub rua: Vec<Uri>,
61    /// `ruf=`: destinations for failure reports (RFC 9991).
62    pub ruf: Vec<Uri>,
63    /// `sp=`: policy for existing subdomains; defaults to `p`.
64    pub sp: Policy,
65    /// `t=`: test mode (`t=y`); the policy is applied one level less strictly.
66    pub t: bool,
67}
68
69#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)]
70/// A reporting destination from the `rua=` or `ruf=` tag of a DMARC record.
71///
72/// Only `mailto:` URIs are kept; the scheme is stripped and the address is
73/// lowercased.
74pub struct Uri {
75    /// The destination email address, without the `mailto:` prefix.
76    pub uri: String,
77    /// Maximum report size in bytes from the `!size` suffix (`k`, `m`, `g`
78    /// and `t` units are expanded); `0` when no limit was given.
79    pub max_size: usize,
80}
81
82#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83/// Identifier alignment mode, the `adkim=` and `aspf=` tags (checked as
84/// described in RFC 9989, Section 4.10.2). Displays as `r` or `s`.
85pub enum Alignment {
86    /// `r`: the identifier's Organizational Domain must match the Author
87    /// Domain's.
88    Relaxed,
89    /// `s`: the identifier must match the Author Domain exactly.
90    Strict,
91}
92
93/// The `psd=` tag of a DMARC record (RFC 9989, Section 4.7).
94///
95/// Both `Yes` and `No` stop the DNS Tree Walk and fix the Organizational
96/// Domain.
97#[derive(Debug, Hash, Clone, PartialEq, Eq)]
98pub enum Psd {
99    /// `psd=y`: the domain is a Public Suffix Domain; the Organizational
100    /// Domain is one label below it.
101    Yes,
102    /// `psd=n`: the domain is not a PSD; it is the Organizational Domain.
103    No,
104    /// `psd=u` or no tag: determined by the Tree Walk.
105    Default,
106}
107
108/// The `fo=` tag of a DMARC record: when to generate failure reports
109/// (RFC 9991).
110#[derive(Debug, Hash, Clone, PartialEq, Eq)]
111pub enum FailureOptions {
112    /// `0`: report when all mechanisms fail to produce an aligned pass
113    /// (default).
114    All,
115    /// `1`: report when any mechanism fails to produce an aligned pass.
116    Any,
117    /// `d`: report when DKIM does not produce an aligned pass.
118    Dkim,
119    /// `s`: report when SPF does not produce an aligned pass.
120    Spf,
121    /// `d:s`: report when either DKIM or SPF does not produce an aligned
122    /// pass.
123    DkimSpf,
124}
125
126#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
127/// A requested handling policy, the `p=`, `sp=` and `np=` tags (RFC 9989,
128/// Section 4.7). Displays as `none`, `quarantine` or `reject`.
129pub enum Policy {
130    /// `none`: no specific action requested.
131    None,
132    /// `quarantine`: treat failing mail as suspicious.
133    Quarantine,
134    /// `reject`: reject failing mail.
135    Reject,
136    /// The tag is absent. Displays as `none`.
137    #[default]
138    Unspecified,
139}
140
141impl Uri {
142    /// Creates a destination (test helper).
143    #[cfg(test)]
144    pub fn new(uri: impl Into<String>, max_size: usize) -> Self {
145        Uri {
146            uri: uri.into(),
147            max_size,
148        }
149    }
150
151    /// Returns the destination email address.
152    pub fn uri(&self) -> &str {
153        &self.uri
154    }
155
156    /// Returns the maximum report size in bytes, or `0` for no limit.
157    pub fn max_size(&self) -> usize {
158        self.max_size
159    }
160}
161
162impl DmarcRecord {
163    /// Returns the failure report destinations (`ruf=`).
164    pub fn ruf(&self) -> &[Uri] {
165        &self.ruf
166    }
167
168    /// Returns the aggregate report destinations (`rua=`).
169    pub fn rua(&self) -> &[Uri] {
170        &self.rua
171    }
172}
173
174impl Display for Alignment {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(match self {
177            Alignment::Relaxed => "r",
178            Alignment::Strict => "s",
179        })
180    }
181}
182
183impl Display for Policy {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.write_str(match self {
186            Policy::Quarantine => "quarantine",
187            Policy::Reject => "reject",
188            Policy::None | Policy::Unspecified => "none",
189        })
190    }
191}
192
193impl AsRef<str> for Uri {
194    fn as_ref(&self) -> &str {
195        &self.uri
196    }
197}