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