mail_auth/dkim/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
7//! DKIM1 verification results: [`DkimResult`] and [`DkimOutput`].
8
9use super::Signature;
10use crate::{DnsError, Error};
11use std::fmt::Display;
12
13/// The outcome of verifying one DKIM1 signature, using the result names of
14/// RFC 8601, Section 2.7.1.
15///
16/// Produced by [`MessageAuthenticator::verify_dkim`](crate::MessageAuthenticator::verify_dkim)
17/// inside each [`DkimOutput`]. Every variant other than `Pass` and `None`
18/// carries the error that caused it.
19#[derive(Debug, PartialEq, Eq, Clone)]
20pub enum DkimResult {
21 /// The signature verified.
22 Pass,
23 /// The signature could not be evaluated: it is malformed, uses an
24 /// unsupported version, algorithm or canonicalization, was ignored for
25 /// carrying `l=` in strict mode, has expired, or its body hash does not
26 /// match the message.
27 Neutral(crate::Error),
28 /// The signature was evaluated and is invalid: the cryptographic check
29 /// failed or the `i=` domain does not match `d=`.
30 Fail(crate::Error),
31 /// A permanent error while fetching the key: the key record does not
32 /// exist, is invalid, has been revoked or uses an unsupported key type.
33 PermError(crate::Error),
34 /// A transient DNS error while fetching the key; retrying later may
35 /// succeed.
36 TempError(crate::Error),
37 /// No signature was evaluated. `verify_dkim` never returns it (a message
38 /// without signatures yields an empty vector); it appears in other
39 /// outputs, such as ARC, when there is nothing to verify.
40 None,
41}
42
43/// The verification result of one DKIM1 signature.
44///
45/// [`MessageAuthenticator::verify_dkim`](crate::MessageAuthenticator::verify_dkim)
46/// returns one output per parsed `DKIM-Signature` header, plus a `neutral`
47/// output without a signature for each header rejected with an
48/// [`Error::Dkim`] error (such as an unsupported algorithm
49/// or version). Headers that fail to parse for other reasons, such as a
50/// missing tag or invalid base64, produce no output. Besides the
51/// [`DkimResult`], it records the parsed signature (when the header could be
52/// parsed), the RFC 6651 failure report address and whether the result comes
53/// from an RFC 6541 ATPS check. Use `From<DkimResult>` and
54/// [`with_signature`](Self::with_signature) to synthesize an output, for
55/// example in tests or when feeding DMARC, which needs the signature's `d=`
56/// domain for alignment.
57#[derive(Debug, PartialEq, Eq, Clone)]
58pub struct DkimOutput<'x> {
59 pub(crate) result: DkimResult,
60 pub(crate) signature: Option<&'x Signature>,
61 pub(crate) report: Option<String>,
62 pub(crate) is_atps: bool,
63}
64
65impl<'x> DkimOutput<'x> {
66 pub(crate) fn pass() -> Self {
67 DkimOutput {
68 result: DkimResult::Pass,
69 signature: None,
70 report: None,
71 is_atps: false,
72 }
73 }
74
75 pub(crate) fn perm_err(err: Error) -> Self {
76 DkimOutput {
77 result: DkimResult::PermError(err),
78 signature: None,
79 report: None,
80 is_atps: false,
81 }
82 }
83
84 pub(crate) fn temp_err(err: Error) -> Self {
85 DkimOutput {
86 result: DkimResult::TempError(err),
87 signature: None,
88 report: None,
89 is_atps: false,
90 }
91 }
92
93 pub(crate) fn fail(err: Error) -> Self {
94 DkimOutput {
95 result: DkimResult::Fail(err),
96 signature: None,
97 report: None,
98 is_atps: false,
99 }
100 }
101
102 pub(crate) fn neutral(err: Error) -> Self {
103 DkimOutput {
104 result: DkimResult::Neutral(err),
105 signature: None,
106 report: None,
107 is_atps: false,
108 }
109 }
110
111 pub(crate) fn dns_error(err: Error) -> Self {
112 if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
113 DkimOutput::temp_err(err)
114 } else {
115 DkimOutput::perm_err(err)
116 }
117 }
118
119 /// Attaches the signature this result refers to.
120 ///
121 /// Used to build an output outside the verifier, for example from a
122 /// cached result, so that DMARC can check its `d=` domain for alignment.
123 pub fn with_signature(mut self, signature: &'x Signature) -> Self {
124 self.signature = signature.into();
125 self
126 }
127
128 pub(crate) fn with_atps(mut self) -> Self {
129 self.is_atps = true;
130 self
131 }
132
133 /// Returns the verification result.
134 pub fn result(&self) -> &DkimResult {
135 &self.result
136 }
137
138 /// Returns the parsed signature this result refers to, or `None` when the
139 /// `DKIM-Signature` header could not be parsed.
140 pub fn signature(&self) -> Option<&Signature> {
141 self.signature
142 }
143
144 /// Returns the address to send an RFC 6651 failure report to.
145 ///
146 /// Set only when the signature has `r=y`, did not pass, the signer's
147 /// `_report._domainkey.<d>` record exists, the `rp=` sampling percentage
148 /// selected this failure and the `rr=` tag requests reports for this kind
149 /// of failure. The address is `<ra>@<d>`.
150 pub fn report_address(&self) -> Option<&str> {
151 self.report.as_deref()
152 }
153
154 /// Returns `true` when the result comes from an RFC 6541 ATPS check: the
155 /// signature verified, its `atps=` domain matched the `From` domain and
156 /// the ATPS record lookup determined the result.
157 pub fn is_atps(&self) -> bool {
158 self.is_atps
159 }
160}
161
162impl From<DkimResult> for DkimOutput<'_> {
163 fn from(result: DkimResult) -> Self {
164 DkimOutput {
165 result,
166 signature: None,
167 report: None,
168 is_atps: false,
169 }
170 }
171}
172
173impl From<Error> for DkimResult {
174 fn from(err: Error) -> Self {
175 if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
176 DkimResult::TempError(err)
177 } else {
178 DkimResult::PermError(err)
179 }
180 }
181}
182
183impl Display for DkimResult {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 DkimResult::Pass => f.write_str("pass"),
187 DkimResult::Fail(err) => write!(f, "fail; {err}"),
188 DkimResult::Neutral(err) => write!(f, "neutral; {err}"),
189 DkimResult::TempError(err) => write!(f, "temp error; {err}"),
190 DkimResult::PermError(err) => write!(f, "perm error; {err}"),
191 DkimResult::None => f.write_str("none"),
192 }
193 }
194}