mail_auth/report/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
7pub mod generate;
8pub mod parse;
9
10use std::fmt::Write;
11use std::net::IpAddr;
12
13use crate::{
14    dmarc::Dmarc,
15    report::{
16        ActionDisposition, Alignment, DKIMAuthResult, Disposition, DkimResult, DmarcResult,
17        PolicyOverride, PolicyOverrideReason, Record, Report, SPFAuthResult, SPFDomainScope,
18        SpfResult,
19    },
20    ArcOutput, DkimOutput, DmarcOutput, SpfOutput,
21};
22
23use super::PolicyPublished;
24
25impl Report {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn version(&self) -> f32 {
31        self.version
32    }
33
34    pub fn with_version(mut self, version: f32) -> Self {
35        self.version = version;
36        self
37    }
38
39    pub fn org_name(&self) -> &str {
40        &self.report_metadata.org_name
41    }
42
43    pub fn with_org_name(mut self, org_name: impl Into<String>) -> Self {
44        self.report_metadata.org_name = org_name.into();
45        self
46    }
47
48    pub fn email(&self) -> &str {
49        &self.report_metadata.email
50    }
51
52    pub fn with_email(mut self, email: impl Into<String>) -> Self {
53        self.report_metadata.email = email.into();
54        self
55    }
56
57    pub fn extra_contact_info(&self) -> Option<&str> {
58        self.report_metadata.extra_contact_info.as_deref()
59    }
60
61    pub fn with_extra_contact_info(mut self, extra_contact_info: impl Into<String>) -> Self {
62        self.report_metadata.extra_contact_info = Some(extra_contact_info.into());
63        self
64    }
65
66    pub fn report_id(&self) -> &str {
67        &self.report_metadata.report_id
68    }
69
70    pub fn with_report_id(mut self, report_id: impl Into<String>) -> Self {
71        self.report_metadata.report_id = report_id.into();
72        self
73    }
74
75    pub fn date_range_begin(&self) -> u64 {
76        self.report_metadata.date_range.begin
77    }
78
79    pub fn with_date_range_begin(mut self, date_range_begin: u64) -> Self {
80        self.report_metadata.date_range.begin = date_range_begin;
81        self
82    }
83
84    pub fn date_range_end(&self) -> u64 {
85        self.report_metadata.date_range.end
86    }
87
88    pub fn with_date_range_end(mut self, date_range_end: u64) -> Self {
89        self.report_metadata.date_range.end = date_range_end;
90        self
91    }
92
93    pub fn error(&self) -> &[String] {
94        &self.report_metadata.error
95    }
96
97    pub fn with_error(mut self, error: impl Into<String>) -> Self {
98        self.report_metadata.error.push(error.into());
99        self
100    }
101
102    pub fn domain(&self) -> &str {
103        &self.policy_published.domain
104    }
105
106    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
107        self.policy_published.domain = domain.into();
108        self
109    }
110
111    pub fn fo(&self) -> Option<&str> {
112        self.policy_published.fo.as_deref()
113    }
114
115    pub fn with_fo(mut self, fo: impl Into<String>) -> Self {
116        self.policy_published.fo = Some(fo.into());
117        self
118    }
119
120    pub fn version_published(&self) -> Option<f32> {
121        self.policy_published.version_published
122    }
123
124    pub fn with_version_published(mut self, version_published: f32) -> Self {
125        self.policy_published.version_published = Some(version_published);
126        self
127    }
128
129    pub fn adkim(&self) -> Alignment {
130        self.policy_published.adkim
131    }
132
133    pub fn with_adkim(mut self, adkim: Alignment) -> Self {
134        self.policy_published.adkim = adkim;
135        self
136    }
137
138    pub fn aspf(&self) -> Alignment {
139        self.policy_published.aspf
140    }
141
142    pub fn with_aspf(mut self, aspf: Alignment) -> Self {
143        self.policy_published.aspf = aspf;
144        self
145    }
146
147    pub fn p(&self) -> Disposition {
148        self.policy_published.p
149    }
150
151    pub fn with_p(mut self, p: Disposition) -> Self {
152        self.policy_published.p = p;
153        self
154    }
155
156    pub fn sp(&self) -> Disposition {
157        self.policy_published.sp
158    }
159
160    pub fn with_sp(mut self, sp: Disposition) -> Self {
161        self.policy_published.sp = sp;
162        self
163    }
164
165    pub fn testing(&self) -> bool {
166        self.policy_published.testing
167    }
168
169    pub fn with_testing(mut self, testing: bool) -> Self {
170        self.policy_published.testing = testing;
171        self
172    }
173
174    pub fn records(&self) -> &[Record] {
175        &self.record
176    }
177
178    pub fn with_record(mut self, record: Record) -> Self {
179        self.record.push(record);
180        self
181    }
182
183    pub fn add_record(&mut self, record: Record) {
184        self.record.push(record);
185    }
186
187    pub fn with_policy_published(mut self, policy_published: PolicyPublished) -> Self {
188        self.policy_published = policy_published;
189        self
190    }
191}
192
193impl Record {
194    pub fn new() -> Self {
195        Record::default()
196    }
197
198    pub fn with_dkim_output(mut self, dkim_output: &[DkimOutput]) -> Self {
199        for dkim in dkim_output {
200            if let Some(signature) = &dkim.signature {
201                let (result, human_result) = match &dkim.result {
202                    crate::DkimResult::Pass => (DkimResult::Pass, None),
203                    crate::DkimResult::Neutral(err) => {
204                        (DkimResult::Neutral, err.to_string().into())
205                    }
206                    crate::DkimResult::Fail(err) => (DkimResult::Fail, err.to_string().into()),
207                    crate::DkimResult::PermError(err) => {
208                        (DkimResult::PermError, err.to_string().into())
209                    }
210                    crate::DkimResult::TempError(err) => {
211                        (DkimResult::TempError, err.to_string().into())
212                    }
213                    crate::DkimResult::None => (DkimResult::None, None),
214                };
215
216                self.auth_results.dkim.push(DKIMAuthResult {
217                    domain: signature.d.to_string(),
218                    selector: signature.s.to_string(),
219                    result,
220                    human_result,
221                });
222            }
223        }
224        self
225    }
226
227    pub fn with_spf_output(mut self, spf_output: &SpfOutput, scope: SPFDomainScope) -> Self {
228        self.auth_results.spf.push(SPFAuthResult {
229            domain: spf_output.domain.to_string(),
230            scope,
231            result: match spf_output.result {
232                crate::SpfResult::Pass => SpfResult::Pass,
233                crate::SpfResult::Fail => SpfResult::Fail,
234                crate::SpfResult::SoftFail => SpfResult::SoftFail,
235                crate::SpfResult::Neutral => SpfResult::Neutral,
236                crate::SpfResult::TempError => SpfResult::TempError,
237                crate::SpfResult::PermError => SpfResult::PermError,
238                crate::SpfResult::None => SpfResult::None,
239            },
240            human_result: None,
241        });
242        self
243    }
244
245    pub fn with_dmarc_output(mut self, dmarc_output: &DmarcOutput) -> Self {
246        self.row.policy_evaluated.disposition = if dmarc_output.dkim_result
247            == crate::DmarcResult::Pass
248            || dmarc_output.spf_result == crate::DmarcResult::Pass
249        {
250            ActionDisposition::Pass
251        } else {
252            match dmarc_output.policy {
253                crate::dmarc::Policy::None => ActionDisposition::None,
254                crate::dmarc::Policy::Quarantine => ActionDisposition::Quarantine,
255                crate::dmarc::Policy::Reject => ActionDisposition::Reject,
256                crate::dmarc::Policy::Unspecified => ActionDisposition::None,
257            }
258        };
259        self.row.policy_evaluated.dkim = (&dmarc_output.dkim_result).into();
260        self.row.policy_evaluated.spf = (&dmarc_output.spf_result).into();
261        self
262    }
263
264    pub fn with_arc_output(mut self, arc_output: &ArcOutput) -> Self {
265        if arc_output.result == crate::DkimResult::Pass {
266            let mut comment = "arc=pass".to_string();
267            for set in arc_output.set.iter().rev() {
268                let seal = &set.seal.header;
269                write!(
270                    &mut comment,
271                    " as[{}].d={} as[{}].s={}",
272                    seal.i, seal.d, seal.i, seal.s
273                )
274                .ok();
275            }
276            self.row
277                .policy_evaluated
278                .reason
279                .push(PolicyOverrideReason::new(PolicyOverride::LocalPolicy).with_comment(comment));
280        }
281        self
282    }
283
284    pub fn source_ip(&self) -> Option<IpAddr> {
285        self.row.source_ip
286    }
287
288    pub fn with_source_ip(mut self, source_ip: IpAddr) -> Self {
289        self.row.source_ip = source_ip.into();
290        self
291    }
292
293    pub fn count(&self) -> u32 {
294        self.row.count
295    }
296
297    pub fn with_count(mut self, count: u32) -> Self {
298        self.row.count = count;
299        self
300    }
301
302    pub fn action_disposition(&self) -> ActionDisposition {
303        self.row.policy_evaluated.disposition
304    }
305
306    pub fn with_action_disposition(mut self, disposition: ActionDisposition) -> Self {
307        self.row.policy_evaluated.disposition = disposition;
308        self
309    }
310
311    pub fn dmarc_dkim_result(&self) -> DmarcResult {
312        self.row.policy_evaluated.dkim
313    }
314
315    pub fn with_dmarc_dkim_result(mut self, dkim: DmarcResult) -> Self {
316        self.row.policy_evaluated.dkim = dkim;
317        self
318    }
319
320    pub fn dmarc_spf_result(&self) -> DmarcResult {
321        self.row.policy_evaluated.spf
322    }
323
324    pub fn with_dmarc_spf_result(mut self, spf: DmarcResult) -> Self {
325        self.row.policy_evaluated.spf = spf;
326        self
327    }
328
329    pub fn policy_override_reason(&self) -> &[PolicyOverrideReason] {
330        &self.row.policy_evaluated.reason
331    }
332
333    pub fn with_policy_override_reason(mut self, reason: PolicyOverrideReason) -> Self {
334        self.row.policy_evaluated.reason.push(reason);
335        self
336    }
337
338    pub fn envelope_from(&self) -> &str {
339        &self.identifiers.envelope_from
340    }
341
342    pub fn with_envelope_from(mut self, envelope_from: impl Into<String>) -> Self {
343        self.identifiers.envelope_from = envelope_from.into();
344        self
345    }
346
347    pub fn header_from(&self) -> &str {
348        &self.identifiers.header_from
349    }
350
351    pub fn with_header_from(mut self, header_from: impl Into<String>) -> Self {
352        self.identifiers.header_from = header_from.into();
353        self
354    }
355
356    pub fn envelope_to(&self) -> Option<&str> {
357        self.identifiers.envelope_to.as_deref()
358    }
359
360    pub fn with_envelope_to(mut self, envelope_to: impl Into<String>) -> Self {
361        self.identifiers.envelope_to = Some(envelope_to.into());
362        self
363    }
364
365    pub fn dkim_auth_result(&self) -> &[DKIMAuthResult] {
366        &self.auth_results.dkim
367    }
368
369    pub fn with_dkim_auth_result(mut self, auth_result: DKIMAuthResult) -> Self {
370        self.auth_results.dkim.push(auth_result);
371        self
372    }
373
374    pub fn spf_auth_result(&self) -> &[SPFAuthResult] {
375        &self.auth_results.spf
376    }
377
378    pub fn with_spf_auth_result(mut self, auth_result: SPFAuthResult) -> Self {
379        self.auth_results.spf.push(auth_result);
380        self
381    }
382}
383
384impl PolicyPublished {
385    pub fn from_record(domain: impl Into<String>, dmarc: &Dmarc) -> Self {
386        PolicyPublished {
387            domain: domain.into(),
388            adkim: (&dmarc.adkim).into(),
389            aspf: (&dmarc.aspf).into(),
390            p: (&dmarc.p).into(),
391            sp: (&dmarc.sp).into(),
392            testing: dmarc.t,
393            fo: match &dmarc.fo {
394                crate::dmarc::Report::All => "0",
395                crate::dmarc::Report::Any => "1",
396                crate::dmarc::Report::Dkim => "d",
397                crate::dmarc::Report::Spf => "s",
398                crate::dmarc::Report::DkimSpf => "d:s",
399            }
400            .to_string()
401            .into(),
402            version_published: None,
403        }
404    }
405}
406
407impl DKIMAuthResult {
408    pub fn new() -> Self {
409        DKIMAuthResult::default()
410    }
411
412    pub fn domain(&self) -> &str {
413        &self.domain
414    }
415
416    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
417        self.domain = domain.into();
418        self
419    }
420
421    pub fn selector(&self) -> &str {
422        &self.selector
423    }
424
425    pub fn with_selector(mut self, selector: impl Into<String>) -> Self {
426        self.selector = selector.into();
427        self
428    }
429
430    pub fn result(&self) -> DkimResult {
431        self.result
432    }
433
434    pub fn with_result(mut self, result: DkimResult) -> Self {
435        self.result = result;
436        self
437    }
438
439    pub fn human_result(&self) -> Option<&str> {
440        self.human_result.as_deref()
441    }
442
443    pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self {
444        self.human_result = Some(human_result.into());
445        self
446    }
447}
448
449impl SPFAuthResult {
450    pub fn new() -> Self {
451        SPFAuthResult::default()
452    }
453
454    pub fn domain(&self) -> &str {
455        &self.domain
456    }
457
458    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
459        self.domain = domain.into();
460        self
461    }
462
463    pub fn scope(&self) -> SPFDomainScope {
464        self.scope
465    }
466
467    pub fn with_scope(mut self, scope: SPFDomainScope) -> Self {
468        self.scope = scope;
469        self
470    }
471
472    pub fn result(&self) -> SpfResult {
473        self.result
474    }
475
476    pub fn with_result(mut self, result: SpfResult) -> Self {
477        self.result = result;
478        self
479    }
480
481    pub fn human_result(&self) -> Option<&str> {
482        self.human_result.as_deref()
483    }
484
485    pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self {
486        self.human_result = Some(human_result.into());
487        self
488    }
489}
490
491impl PolicyOverrideReason {
492    pub fn new(type_: PolicyOverride) -> Self {
493        PolicyOverrideReason {
494            type_,
495            comment: None,
496        }
497    }
498
499    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
500        self.comment = Some(comment.into());
501        self
502    }
503
504    pub fn comment(&self) -> Option<&str> {
505        self.comment.as_deref()
506    }
507
508    pub fn policy_override(&self) -> PolicyOverride {
509        self.type_
510    }
511}
512
513impl From<&crate::DmarcResult> for DmarcResult {
514    fn from(result: &crate::DmarcResult) -> Self {
515        match result {
516            crate::DmarcResult::Pass => DmarcResult::Pass,
517            _ => DmarcResult::Fail,
518        }
519    }
520}
521
522impl From<&crate::dmarc::Alignment> for Alignment {
523    fn from(aligment: &crate::dmarc::Alignment) -> Self {
524        match aligment {
525            crate::dmarc::Alignment::Relaxed => Alignment::Relaxed,
526            crate::dmarc::Alignment::Strict => Alignment::Strict,
527        }
528    }
529}
530
531impl From<&crate::dmarc::Policy> for Disposition {
532    fn from(policy: &crate::dmarc::Policy) -> Self {
533        match policy {
534            crate::dmarc::Policy::None => Disposition::None,
535            crate::dmarc::Policy::Quarantine => Disposition::Quarantine,
536            crate::dmarc::Policy::Reject => Disposition::Reject,
537            crate::dmarc::Policy::Unspecified => Disposition::None,
538        }
539    }
540}