1use 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 }
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 }
865 }
866}