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 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 }
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 }
864 }
865}