Skip to main content

mail_auth/dmarc/
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
7use crate::DnsError;
8use crate::{DmarcOutput, DmarcResult, Error, Version};
9use serde::{Deserialize, Serialize};
10use std::{fmt::Display, sync::Arc};
11
12pub mod parse;
13pub mod verify;
14
15#[derive(Debug, Hash, Clone, PartialEq, Eq)]
16pub struct Dmarc {
17    pub v: Version,
18    pub adkim: Alignment,
19    pub aspf: Alignment,
20    pub fo: Report,
21    pub np: Policy,
22    pub p: Policy,
23    pub psd: Psd,
24    pub rua: Vec<URI>,
25    pub ruf: Vec<URI>,
26    pub sp: Policy,
27    pub t: bool,
28}
29
30#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(
32    feature = "rkyv",
33    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
34)]
35#[allow(clippy::upper_case_acronyms)]
36pub struct URI {
37    pub uri: String,
38    pub max_size: usize,
39}
40
41#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
42pub enum Alignment {
43    Relaxed,
44    Strict,
45}
46
47#[derive(Debug, Hash, Clone, PartialEq, Eq)]
48pub enum Psd {
49    Yes,
50    No,
51    Default,
52}
53
54#[derive(Debug, Hash, Clone, PartialEq, Eq)]
55pub enum Report {
56    All,
57    Any,
58    Dkim,
59    Spf,
60    DkimSpf,
61}
62
63#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
64pub enum Policy {
65    None,
66    Quarantine,
67    Reject,
68    Unspecified,
69}
70
71impl URI {
72    #[cfg(test)]
73    pub fn new(uri: impl Into<String>, max_size: usize) -> Self {
74        URI {
75            uri: uri.into(),
76            max_size,
77        }
78    }
79
80    pub fn uri(&self) -> &str {
81        &self.uri
82    }
83
84    pub fn max_size(&self) -> usize {
85        self.max_size
86    }
87}
88
89impl From<Error> for DmarcResult {
90    fn from(err: Error) -> Self {
91        if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
92            DmarcResult::TempError(err)
93        } else {
94            DmarcResult::PermError(err)
95        }
96    }
97}
98
99impl Default for DmarcOutput {
100    fn default() -> Self {
101        Self {
102            domain: String::new(),
103            policy: Policy::None,
104            record: None,
105            spf_result: DmarcResult::None,
106            dkim_result: DmarcResult::None,
107        }
108    }
109}
110
111impl DmarcOutput {
112    pub fn new(domain: String) -> Self {
113        DmarcOutput {
114            domain,
115            ..Default::default()
116        }
117    }
118
119    pub fn with_domain(mut self, domain: &str) -> Self {
120        self.domain = domain.to_string();
121        self
122    }
123
124    pub fn with_spf_result(mut self, result: DmarcResult) -> Self {
125        self.spf_result = result;
126        self
127    }
128
129    pub fn with_dkim_result(mut self, result: DmarcResult) -> Self {
130        self.dkim_result = result;
131        self
132    }
133
134    pub fn with_record(mut self, record: Arc<Dmarc>) -> Self {
135        self.record = record.into();
136        self
137    }
138
139    pub fn domain(&self) -> &str {
140        &self.domain
141    }
142
143    pub fn into_domain(self) -> String {
144        self.domain
145    }
146
147    pub fn policy(&self) -> Policy {
148        self.policy
149    }
150
151    pub fn dkim_result(&self) -> &DmarcResult {
152        &self.dkim_result
153    }
154
155    pub fn spf_result(&self) -> &DmarcResult {
156        &self.spf_result
157    }
158
159    pub fn result(&self) -> DmarcResult {
160        match self.mechanism_result() {
161            Some(result) => result.clone(),
162            None if self.record.is_some() => DmarcResult::Fail(Error::NotAligned),
163            None => DmarcResult::None,
164        }
165    }
166
167    pub(crate) fn mechanism_result(&self) -> Option<&DmarcResult> {
168        [&self.spf_result, &self.dkim_result]
169            .into_iter()
170            .filter_map(|result| {
171                let rank = match result {
172                    DmarcResult::Pass => 0,
173                    DmarcResult::TempError(_) => 1,
174                    DmarcResult::PermError(_) => 2,
175                    DmarcResult::Fail(_) => 3,
176                    DmarcResult::None => return None,
177                };
178                Some((rank, result))
179            })
180            .min_by_key(|(rank, _)| *rank)
181            .map(|(_, result)| result)
182    }
183
184    pub fn dmarc_record(&self) -> Option<&Dmarc> {
185        self.record.as_deref()
186    }
187
188    pub fn dmarc_record_cloned(&self) -> Option<Arc<Dmarc>> {
189        self.record.clone()
190    }
191
192    pub fn requested_reports(&self) -> bool {
193        self.record
194            .as_ref()
195            .is_some_and(|r| !r.rua.is_empty() || !r.ruf.is_empty())
196    }
197
198    /// Returns the failure reporting options
199    pub fn failure_report(&self) -> Option<Report> {
200        // Send failure reports
201        match &self.record {
202            Some(record)
203                if !record.ruf.is_empty()
204                    && !matches!(self.mechanism_result(), Some(DmarcResult::TempError(_)))
205                    && ((self.dkim_result != DmarcResult::Pass
206                        && matches!(record.fo, Report::Any | Report::Dkim | Report::DkimSpf))
207                        || (self.spf_result != DmarcResult::Pass
208                            && matches!(
209                                record.fo,
210                                Report::Any | Report::Spf | Report::DkimSpf
211                            ))
212                        || (self.dkim_result != DmarcResult::Pass
213                            && self.spf_result != DmarcResult::Pass
214                            && record.fo == Report::All)) =>
215            {
216                Some(record.fo.clone())
217            }
218            _ => None,
219        }
220    }
221}
222
223impl Dmarc {
224    pub fn ruf(&self) -> &[URI] {
225        &self.ruf
226    }
227
228    pub fn rua(&self) -> &[URI] {
229        &self.rua
230    }
231}
232
233impl Display for Policy {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        f.write_str(match self {
236            Policy::Quarantine => "quarantine",
237            Policy::Reject => "reject",
238            Policy::None | Policy::Unspecified => "none",
239        })
240    }
241}
242
243impl AsRef<str> for URI {
244    fn as_ref(&self) -> &str {
245        &self.uri
246    }
247}