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