mail_auth/spf/output.rs
1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL, RR_TEMP_PERM_ERROR, SpfRecord};
8use crate::Error;
9use crate::sampling::is_within_pct;
10use std::{fmt::Display, str::FromStr};
11
12/// The result of an SPF check (RFC 7208, Section 2.6).
13///
14/// Displays as the lowercase RFC token (`pass`, `fail`, `softfail`,
15/// `neutral`, `temperror`, `permerror`, `none`) and parses from the same
16/// tokens, ignoring case, through [`FromStr`] (which fails with
17/// [`Error::Parse`] on any other input).
18#[derive(Debug, PartialEq, Eq, Clone, Copy)]
19pub enum SpfResult {
20 /// The client is authorized to use the domain (Section 2.6.3).
21 Pass,
22 /// The client is explicitly not authorized to use the domain
23 /// (Section 2.6.4).
24 Fail,
25 /// The client is probably not authorized, a weak statement between
26 /// `fail` and `neutral` (Section 2.6.5).
27 SoftFail,
28 /// The domain owner makes no assertion about the client (Section 2.6.2).
29 /// Also returned when no directive matches.
30 Neutral,
31 /// A transient error, usually a DNS failure, prevented the check
32 /// (Section 2.6.6).
33 TempError,
34 /// The published record could not be interpreted, or a processing limit
35 /// was exceeded (Section 2.6.7).
36 PermError,
37 /// No SPF record was found, or the domain is not a valid, fully qualified
38 /// name (Section 2.6.1).
39 None,
40}
41
42/// The outcome of an SPF check.
43///
44/// Returned by [`MessageAuthenticator::verify_spf`] and
45/// [`MessageAuthenticator::check_host`]. Besides the [`SpfResult`], it
46/// carries the checked domain, the explanation string of a `fail` result and
47/// the RFC 6652 report address, when the record requests a report for this
48/// result. [`SpfOutput::new`] and [`SpfOutput::with_result`] build a
49/// synthetic output, for example to feed DMARC with a result obtained
50/// elsewhere.
51///
52/// [`MessageAuthenticator::verify_spf`]: crate::MessageAuthenticator::verify_spf
53/// [`MessageAuthenticator::check_host`]: crate::MessageAuthenticator::check_host
54#[derive(Debug, Eq, Clone)]
55pub struct SpfOutput {
56 pub(crate) result: SpfResult,
57 pub(crate) domain: String,
58 pub(crate) report: Option<String>,
59 pub(crate) explanation: Option<String>,
60 pub(crate) identity: Option<SpfIdentity>,
61}
62
63impl PartialEq for SpfOutput {
64 fn eq(&self, other: &Self) -> bool {
65 self.result == other.result
66 && self.domain == other.domain
67 && self.report == other.report
68 && self.explanation == other.explanation
69 }
70}
71
72#[derive(Debug, PartialEq, Eq, Clone, Copy)]
73pub(crate) enum SpfIdentity {
74 Helo,
75 MailFrom,
76}
77
78impl FromStr for SpfResult {
79 type Err = Error;
80
81 fn from_str(value: &str) -> Result<Self, Self::Err> {
82 hashify::map_ignore_case!(value.as_bytes(), SpfResult,
83 b"pass" => SpfResult::Pass,
84 b"fail" => SpfResult::Fail,
85 b"softfail" => SpfResult::SoftFail,
86 b"neutral" => SpfResult::Neutral,
87 b"temperror" => SpfResult::TempError,
88 b"permerror" => SpfResult::PermError,
89 b"none" => SpfResult::None,
90 )
91 .copied()
92 .ok_or(Error::Parse)
93 }
94}
95
96impl SpfOutput {
97 /// Creates an output for `domain` with result [`SpfResult::None`], no
98 /// explanation and no report address.
99 pub fn new(domain: String) -> Self {
100 SpfOutput {
101 result: SpfResult::None,
102 report: None,
103 explanation: None,
104 domain,
105 identity: None,
106 }
107 }
108
109 pub(crate) fn with_identity(mut self, identity: SpfIdentity) -> Self {
110 self.identity = Some(identity);
111 self
112 }
113
114 /// Sets the result.
115 pub fn with_result(mut self, result: SpfResult) -> Self {
116 self.result = result;
117 self
118 }
119
120 /// Sets the report address to `<ra>@<domain>` when `spf` has an `ra=`
121 /// modifier, its `rr=` bitmask covers the current result and the message
122 /// is sampled by `rp=` (RFC 6652, Section 3). Call it after
123 /// [`SpfOutput::with_result`]; a `pass` result never produces a report.
124 pub fn with_report(mut self, spf: &SpfRecord) -> Self {
125 match &spf.ra {
126 Some(ra)
127 if is_within_pct(spf.rp)
128 && match self.result {
129 SpfResult::Fail => (spf.rr & RR_FAIL) != 0,
130 SpfResult::SoftFail => (spf.rr & RR_SOFTFAIL) != 0,
131 SpfResult::Neutral | SpfResult::None => (spf.rr & RR_NEUTRAL_NONE) != 0,
132 SpfResult::TempError | SpfResult::PermError => {
133 (spf.rr & RR_TEMP_PERM_ERROR) != 0
134 }
135 SpfResult::Pass => false,
136 } =>
137 {
138 let ra = String::from_utf8_lossy(ra);
139 let mut report = String::with_capacity(ra.len() + self.domain.len() + 1);
140 report.push_str(ra.as_ref());
141 report.push('@');
142 report.push_str(&self.domain);
143 self.report = report.into();
144 }
145 _ => (),
146 }
147 self
148 }
149
150 /// Sets the explanation string (RFC 7208, Section 6.2).
151 pub fn with_explanation(mut self, explanation: String) -> Self {
152 self.explanation = explanation.into();
153 self
154 }
155
156 /// Returns the SPF result.
157 pub fn result(&self) -> SpfResult {
158 self.result
159 }
160
161 /// Returns the domain that was checked: the HELO domain or the domain of
162 /// the MAIL FROM address.
163 pub fn domain(&self) -> &str {
164 &self.domain
165 }
166
167 /// Returns the expanded `exp=` explanation, set only on a `fail` result
168 /// whose record has an `exp=` modifier that resolved.
169 pub fn explanation(&self) -> Option<&str> {
170 self.explanation.as_deref()
171 }
172
173 /// Returns the address that requested an authentication failure report
174 /// for this result (RFC 6652), if any.
175 pub fn report_address(&self) -> Option<&str> {
176 self.report.as_deref()
177 }
178}
179
180impl Default for SpfOutput {
181 fn default() -> Self {
182 Self {
183 result: SpfResult::None,
184 domain: Default::default(),
185 report: Default::default(),
186 explanation: Default::default(),
187 identity: None,
188 }
189 }
190}
191
192impl Display for SpfResult {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.write_str(match self {
195 SpfResult::Pass => "pass",
196 SpfResult::Fail => "fail",
197 SpfResult::SoftFail => "softfail",
198 SpfResult::Neutral => "neutral",
199 SpfResult::TempError => "temperror",
200 SpfResult::PermError => "permerror",
201 SpfResult::None => "none",
202 })
203 }
204}