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, Disposition, DkimResult, DmarcResult,
18        PolicyOverride, PolicyOverrideReason, Record, Report, SPFAuthResult, SPFDomainScope,
19        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 records(&self) -> &[Record] {
176        &self.record
177    }
178
179    pub fn with_record(mut self, record: Record) -> Self {
180        self.record.push(record);
181        self
182    }
183
184    pub fn add_record(&mut self, record: Record) {
185        self.record.push(record);
186    }
187
188    pub fn with_policy_published(mut self, policy_published: PolicyPublished) -> Self {
189        self.policy_published = policy_published;
190        self
191    }
192}
193
194impl Record {
195    pub fn new() -> Self {
196        Record::default()
197    }
198
199    pub fn with_dkim_output(mut self, dkim_output: &[DkimOutput]) -> Self {
200        for dkim in dkim_output {
201            if let Some(signature) = &dkim.signature {
202                let (result, human_result) = match &dkim.result {
203                    crate::DkimResult::Pass => (DkimResult::Pass, None),
204                    crate::DkimResult::Neutral(err) => {
205                        (DkimResult::Neutral, err.to_string().into())
206                    }
207                    crate::DkimResult::Fail(err) => (DkimResult::Fail, err.to_string().into()),
208                    crate::DkimResult::PermError(err) => {
209                        (DkimResult::PermError, err.to_string().into())
210                    }
211                    crate::DkimResult::TempError(err) => {
212                        (DkimResult::TempError, err.to_string().into())
213                    }
214                    crate::DkimResult::None => (DkimResult::None, None),
215                };
216
217                self.auth_results.dkim.push(DKIMAuthResult {
218                    domain: signature.d.to_string(),
219                    selector: signature.s.to_string(),
220                    result,
221                    human_result,
222                });
223            }
224        }
225        self
226    }
227
228    pub fn with_dkim2_output(mut self, dkim2_output: &Dkim2Output) -> Self {
229        for link in dkim2_output.chain() {
230            let (result, human_result) = match &link.result {
231                Dkim2Result::Pass => (DkimResult::Pass, None),
232                Dkim2Result::Fail(err) => (DkimResult::Fail, err.to_string().into()),
233                Dkim2Result::PermError(err) => (DkimResult::PermError, err.to_string().into()),
234                Dkim2Result::TempError(err) => (DkimResult::TempError, err.to_string().into()),
235                Dkim2Result::None => (DkimResult::None, None),
236            };
237
238            self.auth_results.dkim.push(DKIMAuthResult {
239                domain: link.signature.d.to_string(),
240                selector: link
241                    .signature
242                    .s
243                    .first()
244                    .map(|value| value.selector.clone())
245                    .unwrap_or_default(),
246                result,
247                human_result,
248            });
249        }
250        self
251    }
252
253    pub fn with_spf_output(mut self, spf_output: &SpfOutput, scope: SPFDomainScope) -> Self {
254        self.auth_results.spf.push(SPFAuthResult {
255            domain: spf_output.domain.to_string(),
256            scope,
257            result: match spf_output.result {
258                crate::SpfResult::Pass => SpfResult::Pass,
259                crate::SpfResult::Fail => SpfResult::Fail,
260                crate::SpfResult::SoftFail => SpfResult::SoftFail,
261                crate::SpfResult::Neutral => SpfResult::Neutral,
262                crate::SpfResult::TempError => SpfResult::TempError,
263                crate::SpfResult::PermError => SpfResult::PermError,
264                crate::SpfResult::None => SpfResult::None,
265            },
266            human_result: None,
267        });
268        self
269    }
270
271    pub fn with_dmarc_output(mut self, dmarc_output: &DmarcOutput) -> Self {
272        self.row.policy_evaluated.disposition = if dmarc_output.dkim_result
273            == crate::DmarcResult::Pass
274            || dmarc_output.spf_result == crate::DmarcResult::Pass
275        {
276            ActionDisposition::Pass
277        } else {
278            match dmarc_output.policy {
279                crate::dmarc::Policy::None => ActionDisposition::None,
280                crate::dmarc::Policy::Quarantine => ActionDisposition::Quarantine,
281                crate::dmarc::Policy::Reject => ActionDisposition::Reject,
282                crate::dmarc::Policy::Unspecified => ActionDisposition::None,
283            }
284        };
285        self.row.policy_evaluated.dkim = (&dmarc_output.dkim_result).into();
286        self.row.policy_evaluated.spf = (&dmarc_output.spf_result).into();
287        self
288    }
289
290    #[cfg(feature = "arc")]
291    pub fn with_arc_output(mut self, arc_output: &ArcOutput) -> Self {
292        if arc_output.result == crate::DkimResult::Pass {
293            let mut comment = "arc=pass".to_string();
294            for set in arc_output.set.iter().rev() {
295                let seal = &set.seal.header;
296                write!(
297                    &mut comment,
298                    " as[{}].d={} as[{}].s={}",
299                    seal.i, seal.d, seal.i, seal.s
300                )
301                .ok();
302            }
303            self.row
304                .policy_evaluated
305                .reason
306                .push(PolicyOverrideReason::new(PolicyOverride::LocalPolicy).with_comment(comment));
307        }
308        self
309    }
310
311    pub fn source_ip(&self) -> Option<IpAddr> {
312        self.row.source_ip
313    }
314
315    pub fn with_source_ip(mut self, source_ip: IpAddr) -> Self {
316        self.row.source_ip = source_ip.into();
317        self
318    }
319
320    pub fn count(&self) -> u32 {
321        self.row.count
322    }
323
324    pub fn with_count(mut self, count: u32) -> Self {
325        self.row.count = count;
326        self
327    }
328
329    pub fn action_disposition(&self) -> ActionDisposition {
330        self.row.policy_evaluated.disposition
331    }
332
333    pub fn with_action_disposition(mut self, disposition: ActionDisposition) -> Self {
334        self.row.policy_evaluated.disposition = disposition;
335        self
336    }
337
338    pub fn dmarc_dkim_result(&self) -> DmarcResult {
339        self.row.policy_evaluated.dkim
340    }
341
342    pub fn with_dmarc_dkim_result(mut self, dkim: DmarcResult) -> Self {
343        self.row.policy_evaluated.dkim = dkim;
344        self
345    }
346
347    pub fn dmarc_spf_result(&self) -> DmarcResult {
348        self.row.policy_evaluated.spf
349    }
350
351    pub fn with_dmarc_spf_result(mut self, spf: DmarcResult) -> Self {
352        self.row.policy_evaluated.spf = spf;
353        self
354    }
355
356    pub fn policy_override_reason(&self) -> &[PolicyOverrideReason] {
357        &self.row.policy_evaluated.reason
358    }
359
360    pub fn with_policy_override_reason(mut self, reason: PolicyOverrideReason) -> Self {
361        self.row.policy_evaluated.reason.push(reason);
362        self
363    }
364
365    pub fn envelope_from(&self) -> &str {
366        &self.identifiers.envelope_from
367    }
368
369    pub fn with_envelope_from(mut self, envelope_from: impl Into<String>) -> Self {
370        self.identifiers.envelope_from = envelope_from.into();
371        self
372    }
373
374    pub fn header_from(&self) -> &str {
375        &self.identifiers.header_from
376    }
377
378    pub fn with_header_from(mut self, header_from: impl Into<String>) -> Self {
379        self.identifiers.header_from = header_from.into();
380        self
381    }
382
383    pub fn envelope_to(&self) -> Option<&str> {
384        self.identifiers.envelope_to.as_deref()
385    }
386
387    pub fn with_envelope_to(mut self, envelope_to: impl Into<String>) -> Self {
388        self.identifiers.envelope_to = Some(envelope_to.into());
389        self
390    }
391
392    pub fn dkim_auth_result(&self) -> &[DKIMAuthResult] {
393        &self.auth_results.dkim
394    }
395
396    pub fn with_dkim_auth_result(mut self, auth_result: DKIMAuthResult) -> Self {
397        self.auth_results.dkim.push(auth_result);
398        self
399    }
400
401    pub fn spf_auth_result(&self) -> &[SPFAuthResult] {
402        &self.auth_results.spf
403    }
404
405    pub fn with_spf_auth_result(mut self, auth_result: SPFAuthResult) -> Self {
406        self.auth_results.spf.push(auth_result);
407        self
408    }
409}
410
411impl PolicyPublished {
412    pub fn from_record(domain: impl Into<String>, dmarc: &Dmarc) -> Self {
413        PolicyPublished {
414            domain: domain.into(),
415            adkim: (&dmarc.adkim).into(),
416            aspf: (&dmarc.aspf).into(),
417            p: (&dmarc.p).into(),
418            sp: (&dmarc.sp).into(),
419            testing: dmarc.t,
420            fo: match &dmarc.fo {
421                crate::dmarc::Report::All => "0",
422                crate::dmarc::Report::Any => "1",
423                crate::dmarc::Report::Dkim => "d",
424                crate::dmarc::Report::Spf => "s",
425                crate::dmarc::Report::DkimSpf => "d:s",
426            }
427            .to_string()
428            .into(),
429            version_published: None,
430        }
431    }
432}
433
434impl DKIMAuthResult {
435    pub fn new() -> Self {
436        DKIMAuthResult::default()
437    }
438
439    pub fn domain(&self) -> &str {
440        &self.domain
441    }
442
443    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
444        self.domain = domain.into();
445        self
446    }
447
448    pub fn selector(&self) -> &str {
449        &self.selector
450    }
451
452    pub fn with_selector(mut self, selector: impl Into<String>) -> Self {
453        self.selector = selector.into();
454        self
455    }
456
457    pub fn result(&self) -> DkimResult {
458        self.result
459    }
460
461    pub fn with_result(mut self, result: DkimResult) -> Self {
462        self.result = result;
463        self
464    }
465
466    pub fn human_result(&self) -> Option<&str> {
467        self.human_result.as_deref()
468    }
469
470    pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self {
471        self.human_result = Some(human_result.into());
472        self
473    }
474}
475
476impl SPFAuthResult {
477    pub fn new() -> Self {
478        SPFAuthResult::default()
479    }
480
481    pub fn domain(&self) -> &str {
482        &self.domain
483    }
484
485    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
486        self.domain = domain.into();
487        self
488    }
489
490    pub fn scope(&self) -> SPFDomainScope {
491        self.scope
492    }
493
494    pub fn with_scope(mut self, scope: SPFDomainScope) -> Self {
495        self.scope = scope;
496        self
497    }
498
499    pub fn result(&self) -> SpfResult {
500        self.result
501    }
502
503    pub fn with_result(mut self, result: SpfResult) -> Self {
504        self.result = result;
505        self
506    }
507
508    pub fn human_result(&self) -> Option<&str> {
509        self.human_result.as_deref()
510    }
511
512    pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self {
513        self.human_result = Some(human_result.into());
514        self
515    }
516}
517
518impl PolicyOverrideReason {
519    pub fn new(type_: PolicyOverride) -> Self {
520        PolicyOverrideReason {
521            type_,
522            comment: None,
523        }
524    }
525
526    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
527        self.comment = Some(comment.into());
528        self
529    }
530
531    pub fn comment(&self) -> Option<&str> {
532        self.comment.as_deref()
533    }
534
535    pub fn policy_override(&self) -> PolicyOverride {
536        self.type_
537    }
538}
539
540impl From<&crate::DmarcResult> for DmarcResult {
541    fn from(result: &crate::DmarcResult) -> Self {
542        match result {
543            crate::DmarcResult::Pass => DmarcResult::Pass,
544            _ => DmarcResult::Fail,
545        }
546    }
547}
548
549impl From<&crate::dmarc::Alignment> for Alignment {
550    fn from(aligment: &crate::dmarc::Alignment) -> Self {
551        match aligment {
552            crate::dmarc::Alignment::Relaxed => Alignment::Relaxed,
553            crate::dmarc::Alignment::Strict => Alignment::Strict,
554        }
555    }
556}
557
558impl From<&crate::dmarc::Policy> for Disposition {
559    fn from(policy: &crate::dmarc::Policy) -> Self {
560        match policy {
561            crate::dmarc::Policy::None => Disposition::None,
562            crate::dmarc::Policy::Quarantine => Disposition::Quarantine,
563            crate::dmarc::Policy::Reject => Disposition::Reject,
564            crate::dmarc::Policy::Unspecified => Disposition::None,
565        }
566    }
567}