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