Skip to main content

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}