Skip to main content

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