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(),
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() == b"feedback" {
164                found_feedback = true;
165            } else if !name.as_ref().is_empty() {
166                return Err(format!(
167                    "Unexpected tag {} at position {}.",
168                    String::from_utf8_lossy(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(),
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(),
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(),
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        let decoder = reader.decoder();
315        while let Some(tag) = reader.next_tag(buf)? {
316            let name = tag.name();
317            hashify::fnc_map!(name.as_ref(),
318                b"extension" => {
319                    let mut e = Extension::default();
320                    if let Ok(Some(attr)) = tag.try_get_attribute("name")
321                        && let Ok(attr) =
322                            attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
323                    {
324                        e.name = attr.to_string();
325                    }
326                    if let Ok(Some(attr)) = tag.try_get_attribute("definition")
327                        && let Ok(attr) =
328                            attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
329                    {
330                        e.definition = attr.to_string();
331                    }
332                    extensions.push(e);
333                    reader.skip_tag(buf)?;
334                },
335                b"" => (),
336                _ => {
337                    reader.skip_tag(buf)?;
338                }
339            );
340        }
341
342        Ok(())
343    }
344}
345
346impl Record {
347    pub(crate) fn parse<R: BufRead>(
348        reader: &mut Reader<R>,
349        buf: &mut Vec<u8>,
350    ) -> Result<Self, String> {
351        let mut r = Record::default();
352
353        while let Some(tag) = reader.next_tag(buf)? {
354            let name = tag.name();
355            hashify::fnc_map!(name.as_ref(),
356                b"row" => {
357                    r.row = Row::parse(reader, buf)?;
358                },
359                b"identifiers" => {
360                    r.identifiers = Identifier::parse(reader, buf)?;
361                },
362                b"auth_results" => {
363                    r.auth_results = AuthResult::parse(reader, buf)?;
364                },
365                b"extensions" => {
366                    Extension::parse(reader, buf, &mut r.extensions)?;
367                },
368                b"" => (),
369                _ => {
370                    reader.skip_tag(buf)?;
371                }
372            );
373        }
374
375        Ok(r)
376    }
377}
378
379impl Row {
380    pub(crate) fn parse<R: BufRead>(
381        reader: &mut Reader<R>,
382        buf: &mut Vec<u8>,
383    ) -> Result<Self, String> {
384        let mut r = Row::default();
385
386        while let Some(tag) = reader.next_tag(buf)? {
387            let name = tag.name();
388            hashify::fnc_map!(name.as_ref(),
389                b"source_ip" => {
390                    if let Some(ip) = reader.next_value::<IpAddr>(buf)? {
391                        r.source_ip = ip.into();
392                    }
393                },
394                b"count" => {
395                    r.count = reader.next_value(buf)?.unwrap_or_default();
396                },
397                b"policy_evaluated" => {
398                    r.policy_evaluated = PolicyEvaluated::parse(reader, buf)?;
399                },
400                b"" => (),
401                _ => {
402                    reader.skip_tag(buf)?;
403                }
404            );
405        }
406
407        Ok(r)
408    }
409}
410
411impl PolicyEvaluated {
412    pub(crate) fn parse<R: BufRead>(
413        reader: &mut Reader<R>,
414        buf: &mut Vec<u8>,
415    ) -> Result<Self, String> {
416        let mut pe = PolicyEvaluated::default();
417
418        while let Some(tag) = reader.next_tag(buf)? {
419            let name = tag.name();
420            hashify::fnc_map!(name.as_ref(),
421                b"disposition" => {
422                    pe.disposition = reader.next_value(buf)?.unwrap_or_default();
423                },
424                b"dkim" => {
425                    pe.dkim = reader.next_value(buf)?.unwrap_or_default();
426                },
427                b"spf" => {
428                    pe.spf = reader.next_value(buf)?.unwrap_or_default();
429                },
430                b"reason" => {
431                    pe.reason.push(PolicyOverrideReason::parse(reader, buf)?);
432                },
433                b"" => (),
434                _ => {
435                    reader.skip_tag(buf)?;
436                }
437            );
438        }
439
440        Ok(pe)
441    }
442}
443
444impl PolicyOverrideReason {
445    pub(crate) fn parse<R: BufRead>(
446        reader: &mut Reader<R>,
447        buf: &mut Vec<u8>,
448    ) -> Result<Self, String> {
449        let mut por = PolicyOverrideReason::default();
450
451        while let Some(tag) = reader.next_tag(buf)? {
452            let name = tag.name();
453            hashify::fnc_map!(name.as_ref(),
454                b"type" => {
455                    por.type_ = reader.next_value(buf)?.unwrap_or_default();
456                },
457                b"comment" => {
458                    por.comment = reader.next_value(buf)?;
459                },
460                b"" => (),
461                _ => {
462                    reader.skip_tag(buf)?;
463                }
464            );
465        }
466
467        Ok(por)
468    }
469}
470
471impl Identifier {
472    pub(crate) fn parse<R: BufRead>(
473        reader: &mut Reader<R>,
474        buf: &mut Vec<u8>,
475    ) -> Result<Self, String> {
476        let mut i = Identifier::default();
477
478        while let Some(tag) = reader.next_tag(buf)? {
479            let name = tag.name();
480            hashify::fnc_map!(name.as_ref(),
481                b"envelope_to" => {
482                    i.envelope_to = reader.next_value(buf)?;
483                },
484                b"envelope_from" => {
485                    i.envelope_from = reader.next_value(buf)?.unwrap_or_default();
486                },
487                b"header_from" => {
488                    i.header_from = reader.next_value(buf)?.unwrap_or_default();
489                },
490                b"" => (),
491                _ => {
492                    reader.skip_tag(buf)?;
493                }
494            );
495        }
496
497        Ok(i)
498    }
499}
500
501impl AuthResult {
502    pub(crate) fn parse<R: BufRead>(
503        reader: &mut Reader<R>,
504        buf: &mut Vec<u8>,
505    ) -> Result<Self, String> {
506        let mut ar = AuthResult::default();
507
508        while let Some(tag) = reader.next_tag(buf)? {
509            let name = tag.name();
510            hashify::fnc_map!(name.as_ref(),
511                b"dkim" => {
512                    ar.dkim.push(DKIMAuthResult::parse(reader, buf)?);
513                },
514                b"spf" => {
515                    ar.spf.push(SPFAuthResult::parse(reader, buf)?);
516                },
517                b"" => (),
518                _ => {
519                    reader.skip_tag(buf)?;
520                }
521            );
522        }
523
524        Ok(ar)
525    }
526}
527
528impl DKIMAuthResult {
529    pub(crate) fn parse<R: BufRead>(
530        reader: &mut Reader<R>,
531        buf: &mut Vec<u8>,
532    ) -> Result<Self, String> {
533        let mut dar = DKIMAuthResult::default();
534
535        while let Some(tag) = reader.next_tag(buf)? {
536            let name = tag.name();
537            hashify::fnc_map!(name.as_ref(),
538                b"domain" => {
539                    dar.domain = reader.next_value(buf)?.unwrap_or_default();
540                },
541                b"selector" => {
542                    dar.selector = reader.next_value(buf)?.unwrap_or_default();
543                },
544                b"result" => {
545                    dar.result = reader.next_value(buf)?.unwrap_or_default();
546                },
547                b"human_result" => {
548                    dar.human_result = reader.next_value(buf)?;
549                },
550                b"" => (),
551                _ => {
552                    reader.skip_tag(buf)?;
553                }
554            );
555        }
556
557        Ok(dar)
558    }
559}
560
561impl SPFAuthResult {
562    pub(crate) fn parse<R: BufRead>(
563        reader: &mut Reader<R>,
564        buf: &mut Vec<u8>,
565    ) -> Result<Self, String> {
566        let mut sar = SPFAuthResult::default();
567
568        while let Some(tag) = reader.next_tag(buf)? {
569            let name = tag.name();
570            hashify::fnc_map!(name.as_ref(),
571                b"domain" => {
572                    sar.domain = reader.next_value(buf)?.unwrap_or_default();
573                },
574                b"scope" => {
575                    sar.scope = reader.next_value(buf)?.unwrap_or_default();
576                },
577                b"result" => {
578                    sar.result = reader.next_value(buf)?.unwrap_or_default();
579                },
580                b"human_result" => {
581                    sar.human_result = reader.next_value(buf)?;
582                },
583                b"" => (),
584                _ => {
585                    reader.skip_tag(buf)?;
586                }
587            );
588        }
589
590        Ok(sar)
591    }
592}
593
594impl FromStr for PolicyOverride {
595    type Err = ();
596
597    fn from_str(s: &str) -> Result<Self, Self::Err> {
598        Ok(hashify::tiny_map!(s.as_bytes(),
599            b"trusted_forwarder" => PolicyOverride::TrustedForwarder,
600            b"mailing_list" => PolicyOverride::MailingList,
601            b"local_policy" => PolicyOverride::LocalPolicy,
602            b"policy_test_mode" => PolicyOverride::PolicyTestMode,
603        )
604        .unwrap_or(PolicyOverride::Other))
605    }
606}
607
608impl FromStr for Discovery {
609    type Err = ();
610
611    fn from_str(s: &str) -> Result<Self, Self::Err> {
612        Ok(hashify::tiny_map!(s.as_bytes(),
613            b"psl" => Discovery::Psl,
614            b"treewalk" => Discovery::Treewalk,
615        )
616        .unwrap_or(Discovery::Unspecified))
617    }
618}
619
620impl FromStr for DmarcResult {
621    type Err = ();
622
623    fn from_str(s: &str) -> Result<Self, Self::Err> {
624        Ok(hashify::tiny_map!(s.as_bytes(),
625            b"pass" => DmarcResult::Pass,
626            b"fail" => DmarcResult::Fail,
627        )
628        .unwrap_or(DmarcResult::Unspecified))
629    }
630}
631
632impl FromStr for DkimResult {
633    type Err = ();
634
635    fn from_str(s: &str) -> Result<Self, Self::Err> {
636        Ok(hashify::tiny_map!(s.as_bytes(),
637            b"none" => DkimResult::None,
638            b"pass" => DkimResult::Pass,
639            b"fail" => DkimResult::Fail,
640            b"policy" => DkimResult::Policy,
641            b"neutral" => DkimResult::Neutral,
642            b"temperror" => DkimResult::TempError,
643            b"permerror" => DkimResult::PermError,
644        )
645        .unwrap_or(DkimResult::None))
646    }
647}
648
649impl FromStr for SpfResult {
650    type Err = ();
651
652    fn from_str(s: &str) -> Result<Self, Self::Err> {
653        Ok(hashify::tiny_map!(s.as_bytes(),
654            b"none" => SpfResult::None,
655            b"pass" => SpfResult::Pass,
656            b"fail" => SpfResult::Fail,
657            b"softfail" => SpfResult::SoftFail,
658            b"neutral" => SpfResult::Neutral,
659            b"temperror" => SpfResult::TempError,
660            b"permerror" => SpfResult::PermError,
661        )
662        .unwrap_or(SpfResult::None))
663    }
664}
665
666impl FromStr for SPFDomainScope {
667    type Err = ();
668
669    fn from_str(s: &str) -> Result<Self, Self::Err> {
670        Ok(hashify::tiny_map!(s.as_bytes(),
671            b"helo" => SPFDomainScope::Helo,
672            b"mfrom" => SPFDomainScope::MailFrom,
673        )
674        .unwrap_or(SPFDomainScope::Unspecified))
675    }
676}
677
678impl FromStr for ActionDisposition {
679    type Err = ();
680
681    fn from_str(s: &str) -> Result<Self, Self::Err> {
682        Ok(hashify::tiny_map!(s.as_bytes(),
683            b"none" => ActionDisposition::None,
684            b"pass" => ActionDisposition::Pass,
685            b"quarantine" => ActionDisposition::Quarantine,
686            b"reject" => ActionDisposition::Reject,
687        )
688        .unwrap_or(ActionDisposition::Unspecified))
689    }
690}
691
692impl FromStr for Disposition {
693    type Err = ();
694
695    fn from_str(s: &str) -> Result<Self, Self::Err> {
696        Ok(hashify::tiny_map!(s.as_bytes(),
697            b"none" => Disposition::None,
698            b"quarantine" => Disposition::Quarantine,
699            b"reject" => Disposition::Reject,
700        )
701        .unwrap_or(Disposition::Unspecified))
702    }
703}
704
705impl FromStr for Alignment {
706    type Err = ();
707
708    fn from_str(s: &str) -> Result<Self, Self::Err> {
709        Ok(match s.as_bytes().first() {
710            Some(b'r') => Alignment::Relaxed,
711            Some(b's') => Alignment::Strict,
712            _ => Alignment::Unspecified,
713        })
714    }
715}
716
717trait ReaderHelper {
718    fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>;
719    fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>;
720    fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>;
721}
722
723impl<R: BufRead> ReaderHelper for Reader<R> {
724    fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> {
725        match self.read_event_into(buf) {
726            Ok(Event::Start(e)) => Ok(Some(e)),
727            Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None),
728            Err(e) => Err(format!(
729                "Error at position {}: {:?}",
730                self.buffer_position(),
731                e
732            )),
733            _ => Ok(Some(BytesStart::new(""))),
734        }
735    }
736
737    fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> {
738        let mut value: Option<String> = None;
739
740        loop {
741            match self.read_event_into(buf) {
742                Ok(Event::Text(e)) => {
743                    let v = e.xml_content(XmlVersion::Implicit1_0).map_err(|e| {
744                        format!(
745                            "Failed to decode text value at position {}: {}",
746                            self.buffer_position(),
747                            e
748                        )
749                    })?;
750                    if let Some(value) = &mut value {
751                        value.push_str(&v);
752                    } else {
753                        value = Some(v.into_owned());
754                    }
755                }
756                Ok(Event::GeneralRef(e)) => {
757                    let v = hashify::tiny_map!(&*e,
758                        b"lt" => "<",
759                        b"gt" => ">",
760                        b"amp" => "&",
761                        b"apos" => "'",
762                        b"quot" => "\"",
763                    )
764                    .map(Cow::Borrowed)
765                    .or_else(|| {
766                        e.resolve_char_ref()
767                            .ok()
768                            .flatten()
769                            .map(|v| Cow::Owned(v.to_string()))
770                    })
771                    .unwrap_or_else(|| e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default());
772
773                    if let Some(value) = &mut value {
774                        value.push_str(&v);
775                    } else {
776                        value = Some(v.into_owned());
777                    }
778                }
779                Ok(Event::End(_)) => {
780                    break;
781                }
782                Ok(Event::Start(e)) => {
783                    return Err(format!(
784                        "Expected value, found unexpected tag {} at position {}.",
785                        String::from_utf8_lossy(e.name().as_ref()),
786                        self.buffer_position()
787                    ));
788                }
789                Ok(Event::Eof) => {
790                    return Err(format!(
791                        "Expected value, found unexpected EOF at position {}.",
792                        self.buffer_position()
793                    ));
794                }
795                _ => (),
796            }
797        }
798
799        Ok(value.and_then(|v| T::from_str(&v).ok()))
800    }
801
802    fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> {
803        let mut tag_count = 0;
804        loop {
805            match self.read_event_into(buf) {
806                Ok(Event::End(_)) => {
807                    if tag_count == 0 {
808                        break;
809                    } else {
810                        tag_count -= 1;
811                    }
812                }
813                Ok(Event::Start(_)) => {
814                    tag_count += 1;
815                }
816                Ok(Event::Eof) => {
817                    return Err(format!(
818                        "Expected value, found unexpected EOF at position {}.",
819                        self.buffer_position()
820                    ));
821                }
822                _ => (),
823            }
824        }
825        Ok(())
826    }
827}
828
829#[cfg(test)]
830mod test {
831    use crate::report::{
832        Discovery, Disposition, Error, PolicyOverride, Report, SPFDomainScope,
833        test_util::{gzip, message_with_attachment, zip},
834    };
835    use std::{fs, path::PathBuf};
836    const MAX_REPORT_SIZE: usize = 25 * 1024 * 1024;
837
838    fn resource(name: &str) -> Vec<u8> {
839        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
840        path.push("resources");
841        path.push("dmarc-feedback");
842        path.push(name);
843        fs::read(path).unwrap()
844    }
845
846    const REPORT: &str = concat!(
847        r#"<?xml version="1.0" encoding="UTF-8"?><feedback><report_metadata>"#,
848        r#"<org_name>Example</org_name><email>dmarc@example.org</email>"#,
849        r#"<report_id>1</report_id><date_range><begin>1</begin><end>2</end></date_range>"#,
850        r#"</report_metadata><policy_published><domain>example.org</domain>"#,
851        r#"</policy_published></feedback>"#
852    );
853
854    #[test]
855    fn dmarc_report_rfc9990_sample() {
856        // RFC 9990 Appendix B sample, exercising the dmarc-2.0 namespace and
857        // the new generator/np/discovery_method/testing elements.
858        let report = Report::parse_xml(&resource("004.xml")).unwrap();
859        assert_eq!(report.domain(), "example.com");
860        assert_eq!(report.np(), Disposition::None);
861        assert_eq!(report.discovery_method(), Discovery::Treewalk);
862        assert_eq!(
863            report.generator(),
864            Some("Example DMARC Aggregate Reporter v1.2")
865        );
866        assert!(!report.testing());
867
868        // The new fields survive a serialize/parse round-trip.
869        let reparsed = Report::parse_xml(report.to_xml().as_bytes()).unwrap();
870        assert_eq!(report, reparsed);
871    }
872
873    #[test]
874    fn dmarc_report_rfc7489_backwards_compat() {
875        // Legacy report: no namespace, "pct", "scope=helo" and the now-removed
876        // "sampled_out" override type must still parse.
877        let report = Report::parse_xml(&resource("005.xml")).unwrap();
878        assert_eq!(report.domain(), "example.com");
879        assert_eq!(report.p(), Disposition::Reject);
880        assert_eq!(report.np(), Disposition::Unspecified);
881        assert_eq!(report.discovery_method(), Discovery::Unspecified);
882        assert_eq!(report.generator(), None);
883
884        let record = &report.records()[0];
885        assert_eq!(
886            record.policy_override_reason()[0].policy_override(),
887            PolicyOverride::Other
888        );
889        assert_eq!(record.spf_auth_result()[0].scope(), SPFDomainScope::Helo);
890    }
891
892    #[test]
893    fn dmarc_report_parse() {
894        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
895        test_dir.push("resources");
896        test_dir.push("dmarc-feedback");
897
898        for file_name in fs::read_dir(&test_dir).unwrap() {
899            let mut file_name = file_name.unwrap().path();
900            if !file_name.extension().unwrap().to_str().unwrap().eq("xml") {
901                continue;
902            }
903            println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
904
905            let feedback = Report::parse_xml(&fs::read(&file_name).unwrap()).unwrap();
906
907            file_name.set_extension("json");
908
909            let expected_feedback =
910                serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
911
912            assert_eq!(expected_feedback, feedback);
913
914            /*fs::write(
915                &file_name,
916                serde_json::to_string_pretty(&feedback).unwrap().as_bytes(),
917            )
918            .unwrap();*/
919        }
920    }
921
922    #[test]
923    fn dmarc_report_eml_parse() {
924        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
925        test_dir.push("resources");
926        test_dir.push("dmarc-feedback");
927
928        for file_name in fs::read_dir(&test_dir).unwrap() {
929            let mut file_name = file_name.unwrap().path();
930            if !file_name.extension().unwrap().to_str().unwrap().eq("eml") {
931                continue;
932            }
933            println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
934
935            let feedback =
936                Report::parse_rfc5322(&fs::read(&file_name).unwrap(), MAX_REPORT_SIZE).unwrap();
937
938            file_name.set_extension("json");
939
940            let expected_feedback =
941                serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
942
943            assert_eq!(expected_feedback, feedback);
944
945            /*fs::write(
946                &file_name,
947                serde_json::to_string_pretty(&feedback).unwrap().as_bytes(),
948            )
949            .unwrap();*/
950        }
951    }
952
953    #[test]
954    fn dmarc_report_zip_forged_size() {
955        let archive = zip("report.xml", REPORT.as_bytes(), None, Some(u32::MAX));
956        let message = message_with_attachment("application/zip", "report.zip", &archive);
957
958        assert_eq!(
959            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
960            Err(Error::ReportTooLarge)
961        );
962    }
963
964    #[test]
965    fn dmarc_report_zip_forged_compressed_size() {
966        let archive = zip("report.xml", REPORT.as_bytes(), Some(u32::MAX), None);
967        let message = message_with_attachment("application/zip", "report.zip", &archive);
968
969        assert!(Report::parse_rfc5322(&message, MAX_REPORT_SIZE).is_err());
970    }
971
972    #[test]
973    fn dmarc_report_zip_within_limit() {
974        let archive = zip("report.xml", REPORT.as_bytes(), None, None);
975        let message = message_with_attachment("application/zip", "report.zip", &archive);
976
977        assert_eq!(
978            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
979            Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
980        );
981        assert_eq!(
982            Report::parse_rfc5322(&message, REPORT.len() - 1),
983            Err(Error::ReportTooLarge)
984        );
985    }
986
987    #[test]
988    fn dmarc_report_gzip_bomb() {
989        let bomb = gzip(&vec![b' '; 1024 * 1024]);
990        let message = message_with_attachment("application/gzip", "report.xml.gz", &bomb);
991
992        assert_eq!(
993            Report::parse_rfc5322(&message, 64 * 1024),
994            Err(Error::ReportTooLarge)
995        );
996    }
997
998    #[test]
999    fn dmarc_report_gzip_within_limit() {
1000        let message = message_with_attachment(
1001            "application/gzip",
1002            "report.xml.gz",
1003            &gzip(REPORT.as_bytes()),
1004        );
1005
1006        assert_eq!(
1007            Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
1008            Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
1009        );
1010        assert_eq!(
1011            Report::parse_rfc5322(&message, REPORT.len() - 1),
1012            Err(Error::ReportTooLarge)
1013        );
1014    }
1015}