mail_auth/spf/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//! Sender Policy Framework (SPF).
8//!
9//! Implements SPF verification as specified in
10//! [RFC 7208](https://datatracker.ietf.org/doc/html/rfc7208), including the
11//! failure reporting extension of
12//! [RFC 6652](https://datatracker.ietf.org/doc/html/rfc6652) (`ra=`, `rp=`
13//! and `rr=` modifiers).
14//!
15//! The entry points are [`MessageAuthenticator::verify_spf`] and
16//! [`MessageAuthenticator::check_host`], which take [`SpfParameters`] and
17//! return an [`SpfOutput`].
18//!
19//! [`MessageAuthenticator::verify_spf`]: crate::MessageAuthenticator::verify_spf
20//! [`MessageAuthenticator::check_host`]: crate::MessageAuthenticator::check_host
21//! [`SpfParameters`]: verify::SpfParameters
22
23/// Macro expansion (RFC 7208, Section 7): evaluation of [`Macro`] strings
24/// against a set of [`Variables`].
25pub mod macros;
26/// Verification result types: [`SpfOutput`] and [`SpfResult`].
27pub mod output;
28/// Parser for `v=spf1` TXT records and `exp=` explanation strings.
29pub mod parse;
30/// The `check_host()` function (RFC 7208, Section 4) and its parameters.
31pub mod verify;
32
33pub(crate) use output::SpfIdentity;
34pub use output::{SpfOutput, SpfResult};
35
36use std::{
37 borrow::Cow,
38 net::{Ipv4Addr, Ipv6Addr},
39};
40
41/// The qualifier prefix of an SPF directive (RFC 7208, Section 4.6.2).
42///
43/// ```text
44/// "+" pass
45/// "-" fail
46/// "~" softfail
47/// "?" neutral
48/// ```
49///
50/// A directive without a qualifier defaults to [`Qualifier::Pass`]. When the
51/// mechanism matches, the qualifier becomes the [`SpfResult`].
52#[derive(Debug, PartialEq, Eq, Clone)]
53pub enum Qualifier {
54 /// `+`: the client is authorized; maps to [`SpfResult::Pass`].
55 Pass,
56 /// `-`: the client is explicitly not authorized; maps to [`SpfResult::Fail`].
57 Fail,
58 /// `~`: the client is probably not authorized; maps to [`SpfResult::SoftFail`].
59 SoftFail,
60 /// `?`: no assertion is made; maps to [`SpfResult::Neutral`].
61 Neutral,
62}
63
64/// An SPF mechanism (RFC 7208, Section 5).
65///
66/// ```text
67/// mechanism = ( all / include
68/// / a / mx / ptr / ip4 / ip6 / exists )
69/// ```
70///
71/// Masks are stored as network bitmasks derived from the CIDR prefix length
72/// at parse time, not as prefix lengths: a `/24` IPv4 prefix is stored as
73/// `0xFFFF_FF00`, a missing prefix as all ones (host match) and `/0` as zero
74/// (matches any address). An address matches when
75/// `ip & mask == addr & mask`.
76#[derive(Debug, PartialEq, Eq, Clone)]
77pub enum Mechanism {
78 /// `all`: always matches (Section 5.1).
79 All,
80 /// `include:<domain-spec>`: matches when the SPF record of the target
81 /// domain evaluates to `pass` (Section 5.2).
82 Include {
83 /// Target domain, as a macro string.
84 macro_string: Macro,
85 },
86 /// `a[:<domain-spec>][/<ip4-cidr>][//<ip6-cidr>]`: matches when the client
87 /// IP is one of the target domain's A or AAAA addresses (Section 5.3).
88 A {
89 /// Target domain, as a macro string; [`Macro::None`] means the
90 /// current `<domain>`.
91 macro_string: Macro,
92 /// IPv4 network bitmask built from the `ip4-cidr-length` (default
93 /// `/32`, all ones).
94 ip4_mask: u32,
95 /// IPv6 network bitmask built from the `ip6-cidr-length` (default
96 /// `/128`, all ones).
97 ip6_mask: u128,
98 },
99 /// `mx[:<domain-spec>][/<ip4-cidr>][//<ip6-cidr>]`: matches when the client
100 /// IP is an address of one of the target domain's MX hosts (Section 5.4).
101 Mx {
102 /// Target domain, as a macro string; [`Macro::None`] means the
103 /// current `<domain>`.
104 macro_string: Macro,
105 /// IPv4 network bitmask built from the `ip4-cidr-length` (default
106 /// `/32`, all ones).
107 ip4_mask: u32,
108 /// IPv6 network bitmask built from the `ip6-cidr-length` (default
109 /// `/128`, all ones).
110 ip6_mask: u128,
111 },
112 /// `ptr[:<domain-spec>]`: matches when a validated reverse DNS name of the
113 /// client IP ends in the target domain (Section 5.5; use is discouraged).
114 Ptr {
115 /// Target domain, as a macro string; [`Macro::None`] means the
116 /// current `<domain>`.
117 macro_string: Macro,
118 },
119 /// `ip4:<ip4-network>[/<ip4-cidr>]`: matches when the client IP is in the
120 /// given IPv4 network (Section 5.6).
121 Ip4 {
122 /// Network address.
123 addr: Ipv4Addr,
124 /// IPv4 network bitmask built from the CIDR prefix length (default
125 /// `/32`, all ones).
126 mask: u32,
127 },
128 /// `ip6:<ip6-network>[/<ip6-cidr>]`: matches when the client IP is in the
129 /// given IPv6 network (Section 5.6).
130 Ip6 {
131 /// Network address.
132 addr: Ipv6Addr,
133 /// IPv6 network bitmask built from the CIDR prefix length (default
134 /// `/128`, all ones).
135 mask: u128,
136 },
137 /// `exists:<domain-spec>`: matches when the target name has an A record
138 /// (Section 5.7).
139 Exists {
140 /// Name to look up, as a macro string.
141 macro_string: Macro,
142 },
143}
144
145/// A single SPF directive (RFC 7208, Section 4.6.2).
146///
147/// ```text
148/// directive = [ qualifier ] mechanism
149/// ```
150#[derive(Debug, PartialEq, Eq, Clone)]
151pub struct Directive {
152 /// Result returned when the mechanism matches.
153 pub qualifier: Qualifier,
154 /// The mechanism to evaluate.
155 pub mechanism: Mechanism,
156}
157
158/// A macro letter (RFC 7208, Section 7.2).
159///
160/// ```text
161/// s = <sender>
162/// l = local-part of <sender>
163/// o = domain of <sender>
164/// d = <domain>
165/// i = <ip>
166/// p = the validated domain name of <ip> (do not use)
167/// v = the string "in-addr" if <ip> is ipv4, or "ip6" if <ip> is ipv6
168/// h = HELO/EHLO domain
169/// ```
170///
171/// The following macro letters are allowed only in `exp` text:
172///
173/// ```text
174/// c = SMTP client IP (easily readable format)
175/// r = domain name of host performing the check
176/// t = current timestamp
177/// ```
178#[derive(Debug, PartialEq, Eq, Clone, Copy)]
179#[repr(u8)]
180pub enum Variable {
181 /// `s`: the full `<sender>` address.
182 Sender = 0,
183 /// `l`: the local part of `<sender>`.
184 SenderLocalPart = 1,
185 /// `o`: the domain of `<sender>`.
186 SenderDomainPart = 2,
187 /// `d`: the `<domain>` currently being evaluated.
188 Domain = 3,
189 /// `i`: the client IP, as dotted decimal (IPv4) or dot-separated nibbles
190 /// (IPv6).
191 Ip = 4,
192 /// `p`: the validated domain name of the client IP (use is discouraged).
193 ValidatedDomain = 5,
194 /// `v`: `in-addr` for IPv4 clients, `ip6` for IPv6 clients.
195 IpVersion = 6,
196 /// `h`: the HELO or EHLO domain.
197 HeloDomain = 7,
198 /// `c`: the client IP in readable form (`exp` text only).
199 SmtpIp = 8,
200 /// `r`: the domain name of the host performing the check (`exp` text only).
201 HostDomain = 9,
202 /// `t`: the current time in seconds since the Unix epoch (`exp` text only).
203 CurrentTime = 10,
204}
205
206/// The values substituted for each [`Variable`] during macro expansion
207/// (RFC 7208, Section 7).
208///
209/// [`MessageAuthenticator::check_host`](crate::MessageAuthenticator::check_host)
210/// fills it from its [`SpfParameters`](verify::SpfParameters); it can also be
211/// built with [`Variables::new`] and the `set_*` methods to expand a
212/// [`Macro`] with [`Macro::eval`].
213#[derive(Debug, PartialEq, Eq, Clone, Default)]
214pub struct Variables<'x> {
215 vars: [Cow<'x, [u8]>; 11],
216 current_time_on_demand: bool,
217}
218
219/// A parsed SPF macro string (RFC 7208, Section 7.1).
220///
221/// Appears as the domain-spec of mechanisms, in the `redirect=` and `exp=`
222/// modifiers, and as the explanation text fetched from the `exp=` target.
223#[derive(Debug, PartialEq, Eq, Clone)]
224pub enum Macro {
225 /// Literal text, copied verbatim.
226 Literal(Box<[u8]>),
227 /// A `%{...}` macro expansion.
228 Variable {
229 /// The macro letter.
230 letter: Variable,
231 /// Number of right-hand parts to keep after splitting; `0` keeps all.
232 num_parts: u32,
233 /// Reverse the order of the parts (`r` transformer).
234 reverse: bool,
235 /// URL-encode the result (uppercase macro letter).
236 escape: bool,
237 /// Bitmask of delimiter characters: bit `n` set means the byte
238 /// `b'+' + n` is a delimiter. Defaults to `.` only.
239 delimiters: u64,
240 },
241 /// A concatenation of literals and variables.
242 List(Box<[Macro]>),
243 /// No macro string was given; [`Macro::eval`] returns the supplied default.
244 None,
245}
246
247/// A parsed SPF record (RFC 7208, Section 4.5).
248///
249/// Parsed from a `v=spf1` DNS TXT record published at the domain being
250/// checked, or at the target of an `include:` mechanism or `redirect=`
251/// modifier.
252#[derive(Debug, PartialEq, Eq, Clone)]
253pub struct SpfRecord {
254 /// Directives in the order they appear in the record.
255 pub directives: Box<[Directive]>,
256 /// `exp=` modifier: domain whose TXT record holds the explanation for a
257 /// `fail` result (Section 6.2).
258 pub exp: Option<Macro>,
259 /// `redirect=` modifier: domain whose record is evaluated when no
260 /// directive matches (Section 6.1).
261 pub redirect: Option<Macro>,
262 /// `ra=` modifier (RFC 6652): local part of the address that receives
263 /// failure reports; the domain is the one being checked.
264 pub ra: Option<Box<[u8]>>,
265 /// `rp=` modifier (RFC 6652): percentage (0 to 100) of failures to report.
266 /// Defaults to 100.
267 pub rp: u8,
268 /// `rr=` modifier (RFC 6652): bitmask of the result types that trigger a
269 /// report (`0x01` temperror or permerror, `0x02` fail, `0x04` softfail,
270 /// `0x08` neutral or none). Defaults to all bits set (`rr=all`).
271 pub rr: u8,
272}
273
274pub(crate) const RR_TEMP_PERM_ERROR: u8 = 0x01;
275pub(crate) const RR_FAIL: u8 = 0x02;
276pub(crate) const RR_SOFTFAIL: u8 = 0x04;
277pub(crate) const RR_NEUTRAL_NONE: u8 = 0x08;
278
279impl Directive {
280 /// Creates a directive from a qualifier and a mechanism.
281 pub fn new(qualifier: Qualifier, mechanism: Mechanism) -> Self {
282 Directive {
283 qualifier,
284 mechanism,
285 }
286 }
287}
288
289impl Mechanism {
290 /// Returns `true` when the mechanism's domain-spec uses the `p` macro,
291 /// which requires a PTR lookup of the client IP before expansion.
292 pub fn needs_ptr(&self) -> bool {
293 match self {
294 Mechanism::All
295 | Mechanism::Ip4 { .. }
296 | Mechanism::Ip6 { .. }
297 | Mechanism::Ptr { .. } => false,
298 Mechanism::Include { macro_string } => macro_string.needs_ptr(),
299 Mechanism::A { macro_string, .. } => macro_string.needs_ptr(),
300 Mechanism::Mx { macro_string, .. } => macro_string.needs_ptr(),
301 Mechanism::Exists { macro_string } => macro_string.needs_ptr(),
302 }
303 }
304}