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}