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