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