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 dmarc_record(&self) -> Option<&Dmarc> {
160        self.record.as_deref()
161    }
162
163    pub fn dmarc_record_cloned(&self) -> Option<Arc<Dmarc>> {
164        self.record.clone()
165    }
166
167    pub fn requested_reports(&self) -> bool {
168        self.record
169            .as_ref()
170            .is_some_and(|r| !r.rua.is_empty() || !r.ruf.is_empty())
171    }
172
173    /// Returns the failure reporting options
174    pub fn failure_report(&self) -> Option<Report> {
175        // Send failure reports
176        match &self.record {
177            Some(record)
178                if !record.ruf.is_empty()
179                    && ((self.dkim_result != DmarcResult::Pass
180                        && matches!(record.fo, Report::Any | Report::Dkim | Report::DkimSpf))
181                        || (self.spf_result != DmarcResult::Pass
182                            && matches!(
183                                record.fo,
184                                Report::Any | Report::Spf | Report::DkimSpf
185                            ))
186                        || (self.dkim_result != DmarcResult::Pass
187                            && self.spf_result != DmarcResult::Pass
188                            && record.fo == Report::All)) =>
189            {
190                Some(record.fo.clone())
191            }
192            _ => None,
193        }
194    }
195}
196
197impl Dmarc {
198    pub fn ruf(&self) -> &[URI] {
199        &self.ruf
200    }
201
202    pub fn rua(&self) -> &[URI] {
203        &self.rua
204    }
205}
206
207impl Display for Policy {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        f.write_str(match self {
210            Policy::Quarantine => "quarantine",
211            Policy::Reject => "reject",
212            Policy::None | Policy::Unspecified => "none",
213        })
214    }
215}
216
217impl AsRef<str> for URI {
218    fn as_ref(&self) -> &str {
219        &self.uri
220    }
221}