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