Skip to main content

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}