Skip to main content

mail_auth/report/dmarc/
parse.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::report::{
8    ActionDisposition, Alignment, AuthResult, DKIMAuthResult, DateRange, Discovery, Disposition,
9    DkimResult, DmarcResult, Error, Extension, Identifier, PolicyEvaluated, PolicyOverride,
10    PolicyOverrideReason, PolicyPublished, Record, Report, ReportMetadata, Row, SPFAuthResult,
11    SPFDomainScope, SpfResult, read_capped,
12};
13use flate2::read::GzDecoder;
14use mail_parser::{MessageParser, MimeHeaders, PartType};
15use quick_xml::XmlVersion;
16use quick_xml::events::{BytesStart, Event};
17use quick_xml::reader::Reader;
18use std::borrow::Cow;
19use std::io::{BufRead, Cursor};
20use std::net::IpAddr;
21use std::str::FromStr;
22
23impl Report {
24    pub fn parse_rfc5322(report: &[u8], max_size: usize) -> Result<Self, Error> {
25        let message = MessageParser::new()
26            .parse(report)
27            .ok_or(Error::MailParseError)?;
28        let mut error = Error::NoReportsFound;
29
30        for part in &message.parts {
31            match &part.body {
32                PartType::Text(report)
33                    if part
34                        .content_type()
35                        .and_then(|ct| ct.subtype())
36                        .is_some_and(|t| t.eq_ignore_ascii_case("xml"))
37                        || part
38                            .attachment_name()
39                            .and_then(|n| n.rsplit_once('.'))
40                            .is_some_and(|(_, e)| e.eq_ignore_ascii_case("xml")) =>
41                {
42                    match Report::parse_xml(report.as_bytes()) {
43                        Ok(feedback) => return Ok(feedback),
44                        Err(err) => {
45                            error = err.into();
46                        }
47                    }
48                }
49                PartType::Binary(report) | PartType::InlineBinary(report) => {
50                    enum ReportType {
51                        Xml,
52                        Gzip,
53                        Zip,
54                    }
55
56                    let (_, ext) = part
57                        .attachment_name()
58                        .unwrap_or("file.none")
59                        .rsplit_once('.')
60                        .unwrap_or(("file", "none"));
61                    let subtype = part
62                        .content_type()
63                        .and_then(|ct| ct.subtype())
64                        .unwrap_or("none");
65                    let rt = if subtype.eq_ignore_ascii_case("gzip") {
66                        ReportType::Gzip
67                    } else if subtype.eq_ignore_ascii_case("zip") {
68                        ReportType::Zip
69                    } else if subtype.eq_ignore_ascii_case("xml") {
70                        ReportType::Xml
71                    } else if ext.eq_ignore_ascii_case("gz") {
72                        ReportType::Gzip
73                    } else if ext.eq_ignore_ascii_case("zip") {
74                        ReportType::Zip
75                    } else if ext.eq_ignore_ascii_case("xml") {
76                        ReportType::Xml
77                    } else {
78                        continue;
79                    };
80
81                    match rt {
82                        ReportType::Gzip => {
83                            let report: &[u8] = report.as_ref();
84                            let buf = read_capped(GzDecoder::new(report), 0, max_size)?;
85
86                            match Report::parse_xml(&buf) {
87                                Ok(feedback) => return Ok(feedback),
88                                Err(err) => {
89                                    error = err.into();
90                                }
91                            }
92                        }
93                        ReportType::Zip => {
94                            let mut archive = zip::ZipArchive::new(Cursor::new(report))
95                                .map_err(|err| Error::UncompressError(err.to_string()))?;
96                            for i in 0..archive.len() {
97                                match archive.by_index(i) {
98                                    Ok(mut file) => {
99                                        let size_hint = file.size();
100                                        let buf = read_capped(&mut file, size_hint, max_size)?;
101                                        match Report::parse_xml(&buf) {
102                                            Ok(feedback) => return Ok(feedback),
103                                            Err(err) => {
104                                                error = err.into();
105                                            }
106                                        }
107                                    }
108                                    Err(err) => {
109                                        error = Error::UncompressError(err.to_string());
110                                    }
111                                }
112                            }
113                        }
114                        ReportType::Xml => match Report::parse_xml(report) {
115                            Ok(feedback) => return Ok(feedback),
116                            Err(err) => {
117                                error = err.into();
118                            }
119                        },
120                    }
121                }
122                _ => (),
123            }
124        }
125
126        Err(error)
127    }
128
129    pub fn parse_xml(report: &[u8]) -> Result<Self, String> {
130        let mut version: f32 = 0.0;
131        let mut report_metadata = None;
132        let mut policy_published = None;
133        let mut record = Vec::new();
134        let mut extensions = Vec::new();
135
136        let mut reader = Reader::from_reader(report);
137        reader.config_mut().trim_text(true);
138
139        let mut buf = Vec::with_capacity(128);
140        let mut found_feedback = false;
141
142        while let Some(tag) = reader.next_tag(&mut buf)? {
143            let name = tag.name();
144            if found_feedback {
145                hashify::fnc_map!(name.as_ref().as_bytes(),
146                    b"version" => {
147                        version = reader.next_value(&mut buf)?.unwrap_or(0.0);
148                    },
149                    b"report_metadata" => {
150                        report_metadata = ReportMetadata::parse(&mut reader, &mut buf)?.into();
151                    },
152                    b"policy_published" => {
153                        policy_published = PolicyPublished::parse(&mut reader, &mut buf)?.into();
154                    },
155                    b"record" => {
156                        record.push(Record::parse(&mut reader, &mut buf)?);
157                    },
158                    b"extensions" => {
159                        Extension::parse(&mut reader, &mut buf, &mut extensions)?;
160                    },
161                    _ => ()
162                );
163            } else if name.as_ref() == "feedback" {
164                found_feedback = true;
165            } else if !name.as_ref().is_empty() {
166                return Err(format!(
167                    "Unexpected tag {} at position {}.",
168                    name.as_ref(),
169                    reader.buffer_position()
170                ));
171            }
172        }
173
174        Ok(Report {
175            version,
176            report_metadata: report_metadata.ok_or("Missing feedback/report_metadata tag.")?,
177            policy_published: policy_published.ok_or("Missing feedback/policy_published tag.")?,
178            record,
179            extensions,
180        })
181    }
182}
183
184impl ReportMetadata {
185    pub(crate) fn parse<R: BufRead>(
186        reader: &mut Reader<R>,
187        buf: &mut Vec<u8>,
188    ) -> Result<Self, String> {
189        let mut rm = ReportMetadata::default();
190
191        while let Some(tag) = reader.next_tag(buf)? {
192            let name = tag.name();
193            hashify::fnc_map!(name.as_ref().as_bytes(),
194                b"org_name" => {
195                    rm.org_name = reader.next_value::<String>(buf)?.unwrap_or_default();
196                },
197                b"email" => {
198                    rm.email = reader.next_value::<String>(buf)?.unwrap_or_default();
199                },
200                b"extra_contact_info" => {
201                    rm.extra_contact_info = reader.next_value::<String>(buf)?;
202                },
203                b"report_id" => {
204                    rm.report_id = reader.next_value::<String>(buf)?.unwrap_or_default();
205                },
206                b"date_range" => {
207                    rm.date_range = DateRange::parse(reader, buf)?;
208                },
209                b"error" => {
210                    if let Some(err) = reader.next_value::<String>(buf)? {
211                        rm.error.push(err);
212                    }
213                },
214                b"generator" => {
215                    rm.generator = reader.next_value::<String>(buf)?;
216                },
217                b"" => (),
218                _ => {
219                    reader.skip_tag(buf)?;
220                }
221            );
222        }
223
224        Ok(rm)
225    }
226}
227
228impl DateRange {
229    pub(crate) fn parse<R: BufRead>(
230        reader: &mut Reader<R>,
231        buf: &mut Vec<u8>,
232    ) -> Result<Self, String> {
233        let mut dr = DateRange::default();
234
235        while let Some(tag) = reader.next_tag(buf)? {
236            let name = tag.name();
237            hashify::fnc_map!(name.as_ref().as_bytes(),
238                b"begin" => {
239                    dr.begin = reader.next_value(buf)?.unwrap_or_default();
240                },
241                b"end" => {
242                    dr.end = reader.next_value(buf)?.unwrap_or_default();
243                },
244                b"" => (),
245                _ => {
246                    reader.skip_tag(buf)?;
247                }
248            );
249        }
250
251        Ok(dr)
252    }
253}
254
255impl PolicyPublished {
256    pub(crate) fn parse<R: BufRead>(
257        reader: &mut Reader<R>,
258        buf: &mut Vec<u8>,
259    ) -> Result<Self, String> {
260        let mut p = PolicyPublished::default();
261
262        while let Some(tag) = reader.next_tag(buf)? {
263            let name = tag.name();
264            hashify::fnc_map!(name.as_ref().as_bytes(),
265                b"domain" => {
266                    p.domain = reader.next_value::<String>(buf)?.unwrap_or_default();
267                },
268                b"version_published" => {
269                    p.version_published = reader.next_value(buf)?;
270                },
271                b"adkim" => {
272                    p.adkim = reader.next_value(buf)?.unwrap_or_default();
273                },
274                b"aspf" => {
275                    p.aspf = reader.next_value(buf)?.unwrap_or_default();
276                },
277                b"p" => {
278                    p.p = reader.next_value(buf)?.unwrap_or_default();
279                },
280                b"sp" => {
281                    p.sp = reader.next_value(buf)?.unwrap_or_default();
282                },
283                b"np" => {
284                    p.np = reader.next_value(buf)?.unwrap_or_default();
285                },
286                b"discovery_method" => {
287                    p.discovery_method = reader.next_value(buf)?.unwrap_or_default();
288                },
289                b"testing" => {
290                    p.testing = reader
291                        .next_value::<String>(buf)?
292                        .is_some_and(|s| s.eq_ignore_ascii_case("y"));
293                },
294                b"fo" => {
295                    p.fo = reader.next_value::<String>(buf)?;
296                },
297                b"" => (),
298                _ => {
299                    reader.skip_tag(buf)?;
300                }
301            );
302        }
303
304        Ok(p)
305    }
306}
307
308impl Extension {
309    pub(crate) fn parse<R: BufRead>(
310        reader: &mut Reader<R>,
311        buf: &mut Vec<u8>,
312        extensions: &mut Vec<Extension>,
313    ) -> Result<(), String> {
314        while let Some(tag) = reader.next_tag(buf)? {
315            let name = tag.name();
316            hashify::fnc_map!(name.as_ref().as_bytes(),
317                b"extension" => {
318                    let mut e = Extension::default();
319                    if let Ok(Some(attr)) = tag.try_get_attribute("name")
320                        && let Ok(attr) = attr.normalized_value(XmlVersion::Implicit1_0)
321                    {
322                        e.name = attr.to_string();
323                    }
324                    if let Ok(Some(attr)) = tag.try_get_attribute("definition")
325                        && let Ok(attr) = attr.normalized_value(XmlVersion::Implicit1_0)
326                    {
327                        e.definition = attr.to_string();
328                    }
329                    extensions.push(e);
330                    reader.skip_tag(buf)?;
331                },
332                b"" => (),
333                _ => {
334                    reader.skip_tag(buf)?;
335                }
336            );
337        }
338
339        Ok(())
340    }
341}
342
343impl Record {
344    pub(crate) fn parse<R: BufRead>(
345        reader: &mut Reader<R>,
346        buf: &mut Vec<u8>,
347    ) -> Result<Self, String> {
348        let mut r = Record::default();
349
350        while let Some(tag) = reader.next_tag(buf)? {
351            let name = tag.name();
352            hashify::fnc_map!(name.as_ref().as_bytes(),
353                b"row" => {
354                    r.row = Row::parse(reader, buf)?;
355                },
356                b"identifiers" => {
357                    r.identifiers = Identifier::parse(reader, buf)?;
358                },
359                b"auth_results" => {
360                    r.auth_results = AuthResult::parse(reader, buf)?;
361                },
362                b"extensions" => {
363                    Extension::parse(reader, buf, &mut r.extensions)?;
364                },
365                b"" => (),
366                _ => {
367                    reader.skip_tag(buf)?;
368                }
369            );
370        }
371
372        Ok(r)
373    }
374}
375
376impl Row {
377    pub(crate) fn parse<R: BufRead>(
378        reader: &mut Reader<R>,
379        buf: &mut Vec<u8>,
380    ) -> Result<Self, String> {
381        let mut r = Row::default();
382
383        while let Some(tag) = reader.next_tag(buf)? {
384            let name = tag.name();
385            hashify::fnc_map!(name.as_ref().as_bytes(),
386                b"source_ip" => {
387                    if let Some(ip) = reader.next_value::<IpAddr>(buf)? {
388                        r.source_ip = ip.into();
389                    }
390                },
391                b"count" => {
392                    r.count = reader.next_value(buf)?.unwrap_or_default();
393                },
394                b"policy_evaluated" => {
395                    r.policy_evaluated = PolicyEvaluated::parse(reader, buf)?;
396                },
397                b"" => (),
398                _ => {
399                    reader.skip_tag(buf)?;
400                }
401            );
402        }
403
404        Ok(r)
405    }
406}
407
408impl PolicyEvaluated {
409    pub(crate) fn parse<R: BufRead>(
410        reader: &mut Reader<R>,
411        buf: &mut Vec<u8>,
412    ) -> Result<Self, String> {
413        let mut pe = PolicyEvaluated::default();
414
415        while let Some(tag) = reader.next_tag(buf)? {
416            let name = tag.name();
417            hashify::fnc_map!(name.as_ref().as_bytes(),
418                b"disposition" => {
419                    pe.disposition = reader.next_value(buf)?.unwrap_or_default();
420                },
421                b"dkim" => {
422                    pe.dkim = reader.next_value(buf)?.unwrap_or_default();
423                },
424                b"spf" => {
425                    pe.spf = reader.next_value(buf)?.unwrap_or_default();
426                },
427                b"reason" => {
428                    pe.reason.push(PolicyOverrideReason::parse(reader, buf)?);
429                },
430                b"" => (),
431                _ => {
432                    reader.skip_tag(buf)?;
433                }
434            );
435        }
436
437        Ok(pe)
438    }
439}
440
441impl PolicyOverrideReason {
442    pub(crate) fn parse<R: BufRead>(
443        reader: &mut Reader<R>,
444        buf: &mut Vec<u8>,
445    ) -> Result<Self, String> {
446        let mut por = PolicyOverrideReason::default();
447
448        while let Some(tag) = reader.next_tag(buf)? {
449            let name = tag.name();
450            hashify::fnc_map!(name.as_ref().as_bytes(),
451                b"type" => {
452                    por.type_ = reader.next_value(buf)?.unwrap_or_default();
453                },
454                b"comment" => {
455                    por.comment = reader.next_value(buf)?;
456                },
457                b"" => (),
458                _ => {
459                    reader.skip_tag(buf)?;
460                }
461            );
462        }
463
464        Ok(por)
465    }
466}
467
468impl Identifier {
469    pub(crate) fn parse<R: BufRead>(
470        reader: &mut Reader<R>,
471        buf: &mut Vec<u8>,
472    ) -> Result<Self, String> {
473        let mut i = Identifier::default();
474
475        while let Some(tag) = reader.next_tag(buf)? {
476            let name = tag.name();
477            hashify::fnc_map!(name.as_ref().as_bytes(),
478                b"envelope_to" => {
479                    i.envelope_to = reader.next_value(buf)?;
480                },
481                b"envelope_from" => {
482                    i.envelope_from = reader.next_value(buf)?.unwrap_or_default();
483                },
484                b"header_from" => {
485                    i.header_from = reader.next_value(buf)?.unwrap_or_default();
486                },
487                b"" => (),
488                _ => {
489                    reader.skip_tag(buf)?;
490                }
491            );
492        }
493
494        Ok(i)
495    }
496}
497
498impl AuthResult {
499    pub(crate) fn parse<R: BufRead>(
500        reader: &mut Reader<R>,
501        buf: &mut Vec<u8>,
502    ) -> Result<Self, String> {
503        let mut ar = AuthResult::default();
504
505        while let Some(tag) = reader.next_tag(buf)? {
506            let name = tag.name();
507            hashify::fnc_map!(name.as_ref().as_bytes(),
508                b"dkim" => {
509                    ar.dkim.push(DKIMAuthResult::parse(reader, buf)?);
510                },
511                b"spf" => {
512                    ar.spf.push(SPFAuthResult::parse(reader, buf)?);
513                },
514                b"" => (),
515                _ => {
516                    reader.skip_tag(buf)?;
517                }
518            );
519        }
520
521        Ok(ar)
522    }
523}
524
525impl DKIMAuthResult {
526    pub(crate) fn parse<R: BufRead>(
527        reader: &mut Reader<R>,
528        buf: &mut Vec<u8>,
529    ) -> Result<Self, String> {
530        let mut dar = DKIMAuthResult::default();
531
532        while let Some(tag) = reader.next_tag(buf)? {
533            let name = tag.name();
534            hashify::fnc_map!(name.as_ref().as_bytes(),
535                b"domain" => {
536                    dar.domain = reader.next_value(buf)?.unwrap_or_default();
537                },
538                b"selector" => {
539                    dar.selector = reader.next_value(buf)?.unwrap_or_default();
540                },
541                b"result" => {
542                    dar.result = reader.next_value(buf)?.unwrap_or_default();
543                },
544                b"human_result" => {
545                    dar.human_result = reader.next_value(buf)?;
546                },
547                b"" => (),
548                _ => {
549                    reader.skip_tag(buf)?;
550                }
551            );
552        }
553
554        Ok(dar)
555    }
556}
557
558impl SPFAuthResult {
559    pub(crate) fn parse<R: BufRead>(
560        reader: &mut Reader<R>,
561        buf: &mut Vec<u8>,
562    ) -> Result<Self, String> {
563        let mut sar = SPFAuthResult::default();
564
565        while let Some(tag) = reader.next_tag(buf)? {
566            let name = tag.name();
567            hashify::fnc_map!(name.as_ref().as_bytes(),
568                b"domain" => {
569                    sar.domain = reader.next_value(buf)?.unwrap_or_default();
570                },
571                b"scope" => {
572                    sar.scope = reader.next_value(buf)?.unwrap_or_default();
573                },
574                b"result" => {
575                    sar.result = reader.next_value(buf)?.unwrap_or_default();
576                },
577                b"human_result" => {
578                    sar.human_result = reader.next_value(buf)?;
579                },
580                b"" => (),
581                _ => {
582                    reader.skip_tag(buf)?;
583                }
584            );
585        }
586
587        Ok(sar)
588    }
589}
590
591impl FromStr for PolicyOverride {
592    type Err = ();
593
594    fn from_str(s: &str) -> Result<Self, Self::Err> {
595        Ok(hashify::tiny_map!(s.as_bytes(),
596            b"trusted_forwarder" => PolicyOverride::TrustedForwarder,
597            b"mailing_list" => PolicyOverride::MailingList,
598            b"local_policy" => PolicyOverride::LocalPolicy,
599            b"policy_test_mode" => PolicyOverride::PolicyTestMode,
600        )
601        .unwrap_or(PolicyOverride::Other))
602    }
603}
604
605impl FromStr for Discovery {
606    type Err = ();
607
608    fn from_str(s: &str) -> Result<Self, Self::Err> {
609        Ok(hashify::tiny_map!(s.as_bytes(),
610            b"psl" => Discovery::Psl,
611            b"treewalk" => Discovery::Treewalk,
612        )
613        .unwrap_or(Discovery::Unspecified))
614    }
615}
616
617impl FromStr for DmarcResult {
618    type Err = ();
619
620    fn from_str(s: &str) -> Result<Self, Self::Err> {
621        Ok(hashify::tiny_map!(s.as_bytes(),
622            b"pass" => DmarcResult::Pass,
623            b"fail" => DmarcResult::Fail,
624        )
625        .unwrap_or(DmarcResult::Unspecified))
626    }
627}
628
629impl FromStr for DkimResult {
630    type Err = ();
631
632    fn from_str(s: &str) -> Result<Self, Self::Err> {
633        Ok(hashify::tiny_map!(s.as_bytes(),
634            b"none" => DkimResult::None,
635            b"pass" => DkimResult::Pass,
636            b"fail" => DkimResult::Fail,
637            b"policy" => DkimResult::Policy,
638            b"neutral" => DkimResult::Neutral,
639            b"temperror" => DkimResult::TempError,
640            b"permerror" => DkimResult::PermError,
641        )
642        .unwrap_or(DkimResult::None))
643    }
644}
645
646impl FromStr for SpfResult {
647    type Err = ();
648
649    fn from_str(s: &str) -> Result<Self, Self::Err> {
650        Ok(hashify::tiny_map!(s.as_bytes(),
651            b"none" => SpfResult::None,
652            b"pass" => SpfResult::Pass,
653            b"fail" => SpfResult::Fail,
654            b"softfail" => SpfResult::SoftFail,
655            b"neutral" => SpfResult::Neutral,
656            b"temperror" => SpfResult::TempError,
657            b"permerror" => SpfResult::PermError,
658        )
659        .unwrap_or(SpfResult::None))
660    }
661}
662
663impl FromStr for SPFDomainScope {
664    type Err = ();
665
666    fn from_str(s: &str) -> Result<Self, Self::Err> {
667        Ok(hashify::tiny_map!(s.as_bytes(),
668            b"helo" => SPFDomainScope::Helo,
669            b"mfrom" => SPFDomainScope::MailFrom,
670        )
671        .unwrap_or(SPFDomainScope::Unspecified))
672    }
673}
674
675impl FromStr for ActionDisposition {
676    type Err = ();
677
678    fn from_str(s: &str) -> Result<Self, Self::Err> {
679        Ok(hashify::tiny_map!(s.as_bytes(),
680            b"none" => ActionDisposition::None,
681            b"pass" => ActionDisposition::Pass,
682            b"quarantine" => ActionDisposition::Quarantine,
683            b"reject" => ActionDisposition::Reject,
684        )
685        .unwrap_or(ActionDisposition::Unspecified))
686    }
687}
688
689impl FromStr for Disposition {
690    type Err = ();
691
692    fn from_str(s: &str) -> Result<Self, Self::Err> {
693        Ok(hashify::tiny_map!(s.as_bytes(),
694            b"none" => Disposition::None,
695            b"quarantine" => Disposition::Quarantine,
696            b"reject" => Disposition::Reject,
697        )
698        .unwrap_or(Disposition::Unspecified))
699    }
700}
701
702impl FromStr for Alignment {
703    type Err = ();
704
705    fn from_str(s: &str) -> Result<Self, Self::Err> {
706        Ok(match s.as_bytes().first() {
707            Some(b'r') => Alignment::Relaxed,
708            Some(b's') => Alignment::Strict,
709            _ => Alignment::Unspecified,
710        })
711    }
712}
713
714trait ReaderHelper {
715    fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>;
716    fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>;
717    fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>;
718}
719
720impl<R: BufRead> ReaderHelper for Reader<R> {
721    fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> {
722        match self.read_event_into(buf) {
723            Ok(Event::Start(e)) => Ok(Some(e)),
724            Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None),
725            Err(e) => Err(format!(
726                "Error at position {}: {:?}",
727                self.buffer_position(),
728                e
729            )),
730            _ => Ok(Some(BytesStart::new(""))),
731        }
732    }
733
734    fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> {
735        let mut value: Option<String> = None;
736
737        loop {
738            match self.read_event_into(buf) {
739                Ok(Event::Text(e)) => {
740                    let v = e.xml_content(XmlVersion::Implicit1_0);
741                    if let Some(value) = &mut value {
742                        value.push_str(&v);
743                    } else {
744                        value = Some(v.into_owned());
745                    }
746                }
747                Ok(Event::GeneralRef(e)) => {
748                    let v = hashify::tiny_map!(e.as_bytes(),
749                        b"lt" => "<",
750                        b"gt" => ">",
751                        b"amp" => "&",
752                        b"apos" => "'",
753                        b"quot" => "\"",
754                    )
755                    .map(Cow::Borrowed)
756                    .or_else(|| {
757                        e.resolve_char_ref()
758                            .ok()
759                            .flatten()
760                            .map(|v| Cow::Owned(v.to_string()))
761                    })
762                    .unwrap_or_else(|| e.xml_content(XmlVersion::Implicit1_0));
763
764                    if let Some(value) = &mut value {
765                        value.push_str(&v);
766                    } else {
767                        value = Some(v.into_owned());
768                    }
769                }
770                Ok(Event::End(_)) => {
771                    break;
772                }
773                Ok(Event::Start(e)) => {
774                    return Err(format!(
775                        "Expected value, found unexpected tag {} at position {}.",
776                        e.name().as_ref(),
777                        self.buffer_position()
778                    ));
779                }
780                Ok(Event::Eof) => {
781                    return Err(format!(
782                        "Expected value, found unexpected EOF at position {}.",
783                        self.buffer_position()
784                    ));
785                }
786                _ => (),
787            }
788        }
789
790        Ok(value.and_then(|v| T::from_str(&v).ok()))
791    }
792
793    fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> {
794        let mut tag_count = 0;
795        loop {
796            match self.read_event_into(buf) {
797                Ok(Event::End(_)) => {
798                    if tag_count == 0 {
799                        break;
800                    } else {
801                        tag_count -= 1;
802                    }
803                }
804                Ok(Event::Start(_)) => {
805                    tag_count += 1;
806                }
807                Ok(Event::Eof) => {
808                    return Err(format!(
809                        "Expected value, found unexpected EOF at position {}.",
810                        self.buffer_position()
811                    ));
812                }
813                _ => (),
814            }
815        }
816        Ok(())
817    }
818}
819
820#[cfg(test)]
821mod test {
822    use crate::report::{
823        Discovery, Disposition, Error, PolicyOverride, Report, SPFDomainScope,
824        test_util::{gzip, message_with_attachment, zip},
825    };
826    use std::{fs, path::PathBuf};
827    const MAX_REPORT_SIZE: usize = 25 * 1024 * 1024;
828
829    fn resource(name: &str) -> Vec<u8> {
830        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
831        path.push("resources");
832        path.push("dmarc-feedback");
833        path.push(name);
834        fs::read(path).unwrap()
835    }
836
837    const REPORT: &str = concat!(
838        r#"<?xml version="1.0" encoding="UTF-8"?><feedback><report_metadata>"#,
839        r#"<org_name>Example</org_name><email>dmarc@example.org</email>"#,
840        r#"<report_id>1</report_id><date_range><begin>1</begin><end>2</end></date_range>"#,
841        r#"</report_metadata><policy_published><domain>example.org</domain>"#,
842        r#"</policy_published></feedback>"#
843    );
844
845    #[test]
846    fn dmarc_report_rfc9990_sample() {
847        // RFC 9990 Appendix B sample, exercising the dmarc-2.0 namespace and
848        // the new generator/np/discovery_method/testing elements.
849        let report = Report::parse_xml(&resource("004.xml")).unwrap();
850        assert_eq!(report.domain(), "example.com");
851        assert_eq!(report.np(), Disposition::None);
852        assert_eq!(report.discovery_method(), Discovery::Treewalk);
853        assert_eq!(
854            report.generator(),
855            Some("Example DMARC Aggregate Reporter v1.2")
856        );
857        assert!(!report.testing());
858
859        // The new fields survive a serialize/parse round-trip.
860        let reparsed = Report::parse_xml(report.to_xml().as_bytes()).unwrap();
861        assert_eq!(report, reparsed);
862    }
863
864    #[test]
865    fn dmarc_report_rfc7489_backwards_compat() {
866        // Legacy report: no namespace, "pct", "scope=helo" and the now-removed
867        // "sampled_out" override type must still parse.
868        let report = Report::parse_xml(&resource("005.xml")).unwrap();
869        assert_eq!(report.domain(), "example.com");
870        assert_eq!(report.p(), Disposition::Reject);
871        assert_eq!(report.np(), Disposition::Unspecified);
872        assert_eq!(report.discovery_method(), Discovery::Unspecified);
873        assert_eq!(report.generator(), None);
874
875        let record = &report.records()[0];
876        assert_eq!(
877            record.policy_override_reason()[0].policy_override(),
878            PolicyOverride::Other
879        );
880        assert_eq!(record.spf_auth_result()[0].scope(), SPFDomainScope::Helo);
881    }
882
883    #[test]
884    fn dmarc_report_parse() {
885        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
886        test_dir.push("resources");
887        test_dir.push("dmarc-feedback");
888
889        for file_name in fs::read_dir(&test_dir).unwrap() {
890            let mut file_name = file_name.unwrap().path();
891            if !file_name.extension().unwrap().to_str().unwrap().eq("xml") {
892                continue;
893            }
894            println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
895
896            let feedback = Report::parse_xml(&fs::read(&file_name).unwrap()).unwrap();
897
898            file_name.set_extension("json");
899
900            let expected_feedback =
901                serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
902
903            assert_eq!(expected_feedback, feedback);
904
905            /*fs::write(
906                &file_name,
907                serde_json::to_string_pretty(&feedback).unwrap().as_bytes(),
908            )
909            .unwrap();*/
910        }
911    }
912
913    #[test]
914    fn dmarc_report_eml_parse() {
915        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
916        test_dir.push("resources");
917        test_dir.push("dmarc-feedback");
918
919        for file_name in fs::read_dir(&test_dir).unwrap() {
920            let mut file_name = file_name.unwrap().path();
921            if !file_name.extension().unwrap().to_str().unwrap().eq("eml") {
922                continue;
923            }
924            println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
925
926            let feedback =
927                Report::parse_rfc5322(&fs::read(&file_name).unwrap(), MAX_REPORT_SIZE).unwrap();
928
929            file_name.set_extension("json");
930
931            let expected_feedback =
932                serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
933
934            assert_eq!(expected_feedback, feedback);
935
936            /*fs::write(
937                &file_name,
938                serde_json::to_string_pretty(&feedback).unwrap().as_bytes(),
939            )
940            .unwrap();*/
941        }
942    }
943
944    #[test]
945    fn dmarc_report_zip_forged_size() {
946        let archive = zip("report.xml", REPORT.as_bytes(), None, Some(u32::MAX));
947        let message = message_with_attachment("application/zip", "report.zip", &archive);
948
949        assert_eq!(
950            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
951            Err(Error::ReportTooLarge)
952        );
953    }
954
955    #[test]
956    fn dmarc_report_zip_forged_compressed_size() {
957        let archive = zip("report.xml", REPORT.as_bytes(), Some(u32::MAX), None);
958        let message = message_with_attachment("application/zip", "report.zip", &archive);
959
960        assert!(Report::parse_rfc5322(&message, MAX_REPORT_SIZE).is_err());
961    }
962
963    #[test]
964    fn dmarc_report_zip_within_limit() {
965        let archive = zip("report.xml", REPORT.as_bytes(), None, None);
966        let message = message_with_attachment("application/zip", "report.zip", &archive);
967
968        assert_eq!(
969            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
970            Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
971        );
972        assert_eq!(
973            Report::parse_rfc5322(&message, REPORT.len() - 1),
974            Err(Error::ReportTooLarge)
975        );
976    }
977
978    #[test]
979    fn dmarc_report_gzip_bomb() {
980        let bomb = gzip(&vec![b' '; 1024 * 1024]);
981        let message = message_with_attachment("application/gzip", "report.xml.gz", &bomb);
982
983        assert_eq!(
984            Report::parse_rfc5322(&message, 64 * 1024),
985            Err(Error::ReportTooLarge)
986        );
987    }
988
989    #[test]
990    fn dmarc_report_gzip_within_limit() {
991        let message = message_with_attachment(
992            "application/gzip",
993            "report.xml.gz",
994            &gzip(REPORT.as_bytes()),
995        );
996
997        assert_eq!(
998            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
999            Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
1000        );
1001        assert_eq!(
1002            Report::parse_rfc5322(&message, REPORT.len() - 1),
1003            Err(Error::ReportTooLarge)
1004        );
1005    }
1006}