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