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