1use crate::report::{
8 ActionDisposition, Alignment, AuthResult, DKIMAuthResult, DateRange, Discovery, Disposition,
9 DkimResult, 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 let name = tag.name();
150 if found_feedback {
151 hashify::fnc_map!(name.as_ref(),
152 b"version" => {
153 version = reader.next_value(&mut buf)?.unwrap_or(0.0);
154 },
155 b"report_metadata" => {
156 report_metadata = ReportMetadata::parse(&mut reader, &mut buf)?.into();
157 },
158 b"policy_published" => {
159 policy_published = PolicyPublished::parse(&mut reader, &mut buf)?.into();
160 },
161 b"record" => {
162 record.push(Record::parse(&mut reader, &mut buf)?);
163 },
164 b"extensions" => {
165 Extension::parse(&mut reader, &mut buf, &mut extensions)?;
166 },
167 _ => ()
168 );
169 } else if name.as_ref() == b"feedback" {
170 found_feedback = true;
171 } else if !name.as_ref().is_empty() {
172 return Err(format!(
173 "Unexpected tag {} at position {}.",
174 String::from_utf8_lossy(name.as_ref()),
175 reader.buffer_position()
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 let name = tag.name();
199 hashify::fnc_map!(name.as_ref(),
200 b"org_name" => {
201 rm.org_name = reader.next_value::<String>(buf)?.unwrap_or_default();
202 },
203 b"email" => {
204 rm.email = reader.next_value::<String>(buf)?.unwrap_or_default();
205 },
206 b"extra_contact_info" => {
207 rm.extra_contact_info = reader.next_value::<String>(buf)?;
208 },
209 b"report_id" => {
210 rm.report_id = reader.next_value::<String>(buf)?.unwrap_or_default();
211 },
212 b"date_range" => {
213 rm.date_range = DateRange::parse(reader, buf)?;
214 },
215 b"error" => {
216 if let Some(err) = reader.next_value::<String>(buf)? {
217 rm.error.push(err);
218 }
219 },
220 b"generator" => {
221 rm.generator = reader.next_value::<String>(buf)?;
222 },
223 b"" => (),
224 _ => {
225 reader.skip_tag(buf)?;
226 }
227 );
228 }
229
230 Ok(rm)
231 }
232}
233
234impl DateRange {
235 pub(crate) fn parse<R: BufRead>(
236 reader: &mut Reader<R>,
237 buf: &mut Vec<u8>,
238 ) -> Result<Self, String> {
239 let mut dr = DateRange::default();
240
241 while let Some(tag) = reader.next_tag(buf)? {
242 let name = tag.name();
243 hashify::fnc_map!(name.as_ref(),
244 b"begin" => {
245 dr.begin = reader.next_value(buf)?.unwrap_or_default();
246 },
247 b"end" => {
248 dr.end = reader.next_value(buf)?.unwrap_or_default();
249 },
250 b"" => (),
251 _ => {
252 reader.skip_tag(buf)?;
253 }
254 );
255 }
256
257 Ok(dr)
258 }
259}
260
261impl PolicyPublished {
262 pub(crate) fn parse<R: BufRead>(
263 reader: &mut Reader<R>,
264 buf: &mut Vec<u8>,
265 ) -> Result<Self, String> {
266 let mut p = PolicyPublished::default();
267
268 while let Some(tag) = reader.next_tag(buf)? {
269 let name = tag.name();
270 hashify::fnc_map!(name.as_ref(),
271 b"domain" => {
272 p.domain = reader.next_value::<String>(buf)?.unwrap_or_default();
273 },
274 b"version_published" => {
275 p.version_published = reader.next_value(buf)?;
276 },
277 b"adkim" => {
278 p.adkim = reader.next_value(buf)?.unwrap_or_default();
279 },
280 b"aspf" => {
281 p.aspf = reader.next_value(buf)?.unwrap_or_default();
282 },
283 b"p" => {
284 p.p = reader.next_value(buf)?.unwrap_or_default();
285 },
286 b"sp" => {
287 p.sp = reader.next_value(buf)?.unwrap_or_default();
288 },
289 b"np" => {
290 p.np = reader.next_value(buf)?.unwrap_or_default();
291 },
292 b"discovery_method" => {
293 p.discovery_method = reader.next_value(buf)?.unwrap_or_default();
294 },
295 b"testing" => {
296 p.testing = reader
297 .next_value::<String>(buf)?
298 .is_some_and(|s| s.eq_ignore_ascii_case("y"));
299 },
300 b"fo" => {
301 p.fo = reader.next_value::<String>(buf)?;
302 },
303 b"" => (),
304 _ => {
305 reader.skip_tag(buf)?;
306 }
307 );
308 }
309
310 Ok(p)
311 }
312}
313
314impl Extension {
315 pub(crate) fn parse<R: BufRead>(
316 reader: &mut Reader<R>,
317 buf: &mut Vec<u8>,
318 extensions: &mut Vec<Extension>,
319 ) -> Result<(), String> {
320 let decoder = reader.decoder();
321 while let Some(tag) = reader.next_tag(buf)? {
322 let name = tag.name();
323 hashify::fnc_map!(name.as_ref(),
324 b"extension" => {
325 let mut e = Extension::default();
326 if let Ok(Some(attr)) = tag.try_get_attribute("name")
327 && let Ok(attr) =
328 attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
329 {
330 e.name = attr.to_string();
331 }
332 if let Ok(Some(attr)) = tag.try_get_attribute("definition")
333 && let Ok(attr) =
334 attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
335 {
336 e.definition = attr.to_string();
337 }
338 extensions.push(e);
339 reader.skip_tag(buf)?;
340 },
341 b"" => (),
342 _ => {
343 reader.skip_tag(buf)?;
344 }
345 );
346 }
347
348 Ok(())
349 }
350}
351
352impl Record {
353 pub(crate) fn parse<R: BufRead>(
354 reader: &mut Reader<R>,
355 buf: &mut Vec<u8>,
356 ) -> Result<Self, String> {
357 let mut r = Record::default();
358
359 while let Some(tag) = reader.next_tag(buf)? {
360 let name = tag.name();
361 hashify::fnc_map!(name.as_ref(),
362 b"row" => {
363 r.row = Row::parse(reader, buf)?;
364 },
365 b"identifiers" => {
366 r.identifiers = Identifier::parse(reader, buf)?;
367 },
368 b"auth_results" => {
369 r.auth_results = AuthResult::parse(reader, buf)?;
370 },
371 b"extensions" => {
372 Extension::parse(reader, buf, &mut r.extensions)?;
373 },
374 b"" => (),
375 _ => {
376 reader.skip_tag(buf)?;
377 }
378 );
379 }
380
381 Ok(r)
382 }
383}
384
385impl Row {
386 pub(crate) fn parse<R: BufRead>(
387 reader: &mut Reader<R>,
388 buf: &mut Vec<u8>,
389 ) -> Result<Self, String> {
390 let mut r = Row::default();
391
392 while let Some(tag) = reader.next_tag(buf)? {
393 let name = tag.name();
394 hashify::fnc_map!(name.as_ref(),
395 b"source_ip" => {
396 if let Some(ip) = reader.next_value::<IpAddr>(buf)? {
397 r.source_ip = ip.into();
398 }
399 },
400 b"count" => {
401 r.count = reader.next_value(buf)?.unwrap_or_default();
402 },
403 b"policy_evaluated" => {
404 r.policy_evaluated = PolicyEvaluated::parse(reader, buf)?;
405 },
406 b"" => (),
407 _ => {
408 reader.skip_tag(buf)?;
409 }
410 );
411 }
412
413 Ok(r)
414 }
415}
416
417impl PolicyEvaluated {
418 pub(crate) fn parse<R: BufRead>(
419 reader: &mut Reader<R>,
420 buf: &mut Vec<u8>,
421 ) -> Result<Self, String> {
422 let mut pe = PolicyEvaluated::default();
423
424 while let Some(tag) = reader.next_tag(buf)? {
425 let name = tag.name();
426 hashify::fnc_map!(name.as_ref(),
427 b"disposition" => {
428 pe.disposition = reader.next_value(buf)?.unwrap_or_default();
429 },
430 b"dkim" => {
431 pe.dkim = reader.next_value(buf)?.unwrap_or_default();
432 },
433 b"spf" => {
434 pe.spf = reader.next_value(buf)?.unwrap_or_default();
435 },
436 b"reason" => {
437 pe.reason.push(PolicyOverrideReason::parse(reader, buf)?);
438 },
439 b"" => (),
440 _ => {
441 reader.skip_tag(buf)?;
442 }
443 );
444 }
445
446 Ok(pe)
447 }
448}
449
450impl PolicyOverrideReason {
451 pub(crate) fn parse<R: BufRead>(
452 reader: &mut Reader<R>,
453 buf: &mut Vec<u8>,
454 ) -> Result<Self, String> {
455 let mut por = PolicyOverrideReason::default();
456
457 while let Some(tag) = reader.next_tag(buf)? {
458 let name = tag.name();
459 hashify::fnc_map!(name.as_ref(),
460 b"type" => {
461 por.type_ = reader.next_value(buf)?.unwrap_or_default();
462 },
463 b"comment" => {
464 por.comment = reader.next_value(buf)?;
465 },
466 b"" => (),
467 _ => {
468 reader.skip_tag(buf)?;
469 }
470 );
471 }
472
473 Ok(por)
474 }
475}
476
477impl Identifier {
478 pub(crate) fn parse<R: BufRead>(
479 reader: &mut Reader<R>,
480 buf: &mut Vec<u8>,
481 ) -> Result<Self, String> {
482 let mut i = Identifier::default();
483
484 while let Some(tag) = reader.next_tag(buf)? {
485 let name = tag.name();
486 hashify::fnc_map!(name.as_ref(),
487 b"envelope_to" => {
488 i.envelope_to = reader.next_value(buf)?;
489 },
490 b"envelope_from" => {
491 i.envelope_from = reader.next_value(buf)?.unwrap_or_default();
492 },
493 b"header_from" => {
494 i.header_from = reader.next_value(buf)?.unwrap_or_default();
495 },
496 b"" => (),
497 _ => {
498 reader.skip_tag(buf)?;
499 }
500 );
501 }
502
503 Ok(i)
504 }
505}
506
507impl AuthResult {
508 pub(crate) fn parse<R: BufRead>(
509 reader: &mut Reader<R>,
510 buf: &mut Vec<u8>,
511 ) -> Result<Self, String> {
512 let mut ar = AuthResult::default();
513
514 while let Some(tag) = reader.next_tag(buf)? {
515 let name = tag.name();
516 hashify::fnc_map!(name.as_ref(),
517 b"dkim" => {
518 ar.dkim.push(DKIMAuthResult::parse(reader, buf)?);
519 },
520 b"spf" => {
521 ar.spf.push(SPFAuthResult::parse(reader, buf)?);
522 },
523 b"" => (),
524 _ => {
525 reader.skip_tag(buf)?;
526 }
527 );
528 }
529
530 Ok(ar)
531 }
532}
533
534impl DKIMAuthResult {
535 pub(crate) fn parse<R: BufRead>(
536 reader: &mut Reader<R>,
537 buf: &mut Vec<u8>,
538 ) -> Result<Self, String> {
539 let mut dar = DKIMAuthResult::default();
540
541 while let Some(tag) = reader.next_tag(buf)? {
542 let name = tag.name();
543 hashify::fnc_map!(name.as_ref(),
544 b"domain" => {
545 dar.domain = reader.next_value(buf)?.unwrap_or_default();
546 },
547 b"selector" => {
548 dar.selector = reader.next_value(buf)?.unwrap_or_default();
549 },
550 b"result" => {
551 dar.result = reader.next_value(buf)?.unwrap_or_default();
552 },
553 b"human_result" => {
554 dar.human_result = reader.next_value(buf)?;
555 },
556 b"" => (),
557 _ => {
558 reader.skip_tag(buf)?;
559 }
560 );
561 }
562
563 Ok(dar)
564 }
565}
566
567impl SPFAuthResult {
568 pub(crate) fn parse<R: BufRead>(
569 reader: &mut Reader<R>,
570 buf: &mut Vec<u8>,
571 ) -> Result<Self, String> {
572 let mut sar = SPFAuthResult::default();
573
574 while let Some(tag) = reader.next_tag(buf)? {
575 let name = tag.name();
576 hashify::fnc_map!(name.as_ref(),
577 b"domain" => {
578 sar.domain = reader.next_value(buf)?.unwrap_or_default();
579 },
580 b"scope" => {
581 sar.scope = reader.next_value(buf)?.unwrap_or_default();
582 },
583 b"result" => {
584 sar.result = reader.next_value(buf)?.unwrap_or_default();
585 },
586 b"human_result" => {
587 sar.human_result = reader.next_value(buf)?;
588 },
589 b"" => (),
590 _ => {
591 reader.skip_tag(buf)?;
592 }
593 );
594 }
595
596 Ok(sar)
597 }
598}
599
600impl FromStr for PolicyOverride {
601 type Err = ();
602
603 fn from_str(s: &str) -> Result<Self, Self::Err> {
604 Ok(hashify::tiny_map!(s.as_bytes(),
605 b"trusted_forwarder" => PolicyOverride::TrustedForwarder,
606 b"mailing_list" => PolicyOverride::MailingList,
607 b"local_policy" => PolicyOverride::LocalPolicy,
608 b"policy_test_mode" => PolicyOverride::PolicyTestMode,
609 )
610 .unwrap_or(PolicyOverride::Other))
611 }
612}
613
614impl FromStr for Discovery {
615 type Err = ();
616
617 fn from_str(s: &str) -> Result<Self, Self::Err> {
618 Ok(hashify::tiny_map!(s.as_bytes(),
619 b"psl" => Discovery::Psl,
620 b"treewalk" => Discovery::Treewalk,
621 )
622 .unwrap_or(Discovery::Unspecified))
623 }
624}
625
626impl FromStr for DmarcResult {
627 type Err = ();
628
629 fn from_str(s: &str) -> Result<Self, Self::Err> {
630 Ok(hashify::tiny_map!(s.as_bytes(),
631 b"pass" => DmarcResult::Pass,
632 b"fail" => DmarcResult::Fail,
633 )
634 .unwrap_or(DmarcResult::Unspecified))
635 }
636}
637
638impl FromStr for DkimResult {
639 type Err = ();
640
641 fn from_str(s: &str) -> Result<Self, Self::Err> {
642 Ok(hashify::tiny_map!(s.as_bytes(),
643 b"none" => DkimResult::None,
644 b"pass" => DkimResult::Pass,
645 b"fail" => DkimResult::Fail,
646 b"policy" => DkimResult::Policy,
647 b"neutral" => DkimResult::Neutral,
648 b"temperror" => DkimResult::TempError,
649 b"permerror" => DkimResult::PermError,
650 )
651 .unwrap_or(DkimResult::None))
652 }
653}
654
655impl FromStr for SpfResult {
656 type Err = ();
657
658 fn from_str(s: &str) -> Result<Self, Self::Err> {
659 Ok(hashify::tiny_map!(s.as_bytes(),
660 b"none" => SpfResult::None,
661 b"pass" => SpfResult::Pass,
662 b"fail" => SpfResult::Fail,
663 b"softfail" => SpfResult::SoftFail,
664 b"neutral" => SpfResult::Neutral,
665 b"temperror" => SpfResult::TempError,
666 b"permerror" => SpfResult::PermError,
667 )
668 .unwrap_or(SpfResult::None))
669 }
670}
671
672impl FromStr for SPFDomainScope {
673 type Err = ();
674
675 fn from_str(s: &str) -> Result<Self, Self::Err> {
676 Ok(hashify::tiny_map!(s.as_bytes(),
677 b"helo" => SPFDomainScope::Helo,
678 b"mfrom" => SPFDomainScope::MailFrom,
679 )
680 .unwrap_or(SPFDomainScope::Unspecified))
681 }
682}
683
684impl FromStr for ActionDisposition {
685 type Err = ();
686
687 fn from_str(s: &str) -> Result<Self, Self::Err> {
688 Ok(hashify::tiny_map!(s.as_bytes(),
689 b"none" => ActionDisposition::None,
690 b"pass" => ActionDisposition::Pass,
691 b"quarantine" => ActionDisposition::Quarantine,
692 b"reject" => ActionDisposition::Reject,
693 )
694 .unwrap_or(ActionDisposition::Unspecified))
695 }
696}
697
698impl FromStr for Disposition {
699 type Err = ();
700
701 fn from_str(s: &str) -> Result<Self, Self::Err> {
702 Ok(hashify::tiny_map!(s.as_bytes(),
703 b"none" => Disposition::None,
704 b"quarantine" => Disposition::Quarantine,
705 b"reject" => Disposition::Reject,
706 )
707 .unwrap_or(Disposition::Unspecified))
708 }
709}
710
711impl FromStr for Alignment {
712 type Err = ();
713
714 fn from_str(s: &str) -> Result<Self, Self::Err> {
715 Ok(match s.as_bytes().first() {
716 Some(b'r') => Alignment::Relaxed,
717 Some(b's') => Alignment::Strict,
718 _ => Alignment::Unspecified,
719 })
720 }
721}
722
723trait ReaderHelper {
724 fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>;
725 fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>;
726 fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>;
727}
728
729impl<R: BufRead> ReaderHelper for Reader<R> {
730 fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> {
731 match self.read_event_into(buf) {
732 Ok(Event::Start(e)) => Ok(Some(e)),
733 Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None),
734 Err(e) => Err(format!(
735 "Error at position {}: {:?}",
736 self.buffer_position(),
737 e
738 )),
739 _ => Ok(Some(BytesStart::new(""))),
740 }
741 }
742
743 fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> {
744 let mut value: Option<String> = None;
745
746 loop {
747 match self.read_event_into(buf) {
748 Ok(Event::Text(e)) => {
749 let v = e.xml_content(XmlVersion::Implicit1_0).map_err(|e| {
750 format!(
751 "Failed to decode text value at position {}: {}",
752 self.buffer_position(),
753 e
754 )
755 })?;
756 if let Some(value) = &mut value {
757 value.push_str(&v);
758 } else {
759 value = Some(v.into_owned());
760 }
761 }
762 Ok(Event::GeneralRef(e)) => {
763 let v = hashify::tiny_map!(&*e,
764 b"lt" => "<",
765 b"gt" => ">",
766 b"amp" => "&",
767 b"apos" => "'",
768 b"quot" => "\"",
769 )
770 .map(Cow::Borrowed)
771 .or_else(|| {
772 e.resolve_char_ref()
773 .ok()
774 .flatten()
775 .map(|v| Cow::Owned(v.to_string()))
776 })
777 .unwrap_or_else(|| e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default());
778
779 if let Some(value) = &mut value {
780 value.push_str(&v);
781 } else {
782 value = Some(v.into_owned());
783 }
784 }
785 Ok(Event::End(_)) => {
786 break;
787 }
788 Ok(Event::Start(e)) => {
789 return Err(format!(
790 "Expected value, found unexpected tag {} at position {}.",
791 String::from_utf8_lossy(e.name().as_ref()),
792 self.buffer_position()
793 ));
794 }
795 Ok(Event::Eof) => {
796 return Err(format!(
797 "Expected value, found unexpected EOF at position {}.",
798 self.buffer_position()
799 ));
800 }
801 _ => (),
802 }
803 }
804
805 Ok(value.and_then(|v| T::from_str(&v).ok()))
806 }
807
808 fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> {
809 let mut tag_count = 0;
810 loop {
811 match self.read_event_into(buf) {
812 Ok(Event::End(_)) => {
813 if tag_count == 0 {
814 break;
815 } else {
816 tag_count -= 1;
817 }
818 }
819 Ok(Event::Start(_)) => {
820 tag_count += 1;
821 }
822 Ok(Event::Eof) => {
823 return Err(format!(
824 "Expected value, found unexpected EOF at position {}.",
825 self.buffer_position()
826 ));
827 }
828 _ => (),
829 }
830 }
831 Ok(())
832 }
833}
834
835#[cfg(test)]
836mod test {
837 use std::{fs, path::PathBuf};
838
839 use crate::report::{Discovery, Disposition, PolicyOverride, Report, SPFDomainScope};
840
841 fn resource(name: &str) -> Vec<u8> {
842 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
843 path.push("resources");
844 path.push("dmarc-feedback");
845 path.push(name);
846 fs::read(path).unwrap()
847 }
848
849 #[test]
850 fn dmarc_report_rfc9990_sample() {
851 let report = Report::parse_xml(&resource("004.xml")).unwrap();
854 assert_eq!(report.domain(), "example.com");
855 assert_eq!(report.np(), Disposition::None);
856 assert_eq!(report.discovery_method(), Discovery::Treewalk);
857 assert_eq!(
858 report.generator(),
859 Some("Example DMARC Aggregate Reporter v1.2")
860 );
861 assert!(!report.testing());
862
863 let reparsed = Report::parse_xml(report.to_xml().as_bytes()).unwrap();
865 assert_eq!(report, reparsed);
866 }
867
868 #[test]
869 fn dmarc_report_rfc7489_backwards_compat() {
870 let report = Report::parse_xml(&resource("005.xml")).unwrap();
873 assert_eq!(report.domain(), "example.com");
874 assert_eq!(report.p(), Disposition::Reject);
875 assert_eq!(report.np(), Disposition::Unspecified);
876 assert_eq!(report.discovery_method(), Discovery::Unspecified);
877 assert_eq!(report.generator(), None);
878
879 let record = &report.records()[0];
880 assert_eq!(
881 record.policy_override_reason()[0].policy_override(),
882 PolicyOverride::Other
883 );
884 assert_eq!(record.spf_auth_result()[0].scope(), SPFDomainScope::Helo);
885 }
886
887 #[test]
888 fn dmarc_report_parse() {
889 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
890 test_dir.push("resources");
891 test_dir.push("dmarc-feedback");
892
893 for file_name in fs::read_dir(&test_dir).unwrap() {
894 let mut file_name = file_name.unwrap().path();
895 if !file_name.extension().unwrap().to_str().unwrap().eq("xml") {
896 continue;
897 }
898 println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
899
900 let feedback = Report::parse_xml(&fs::read(&file_name).unwrap()).unwrap();
901
902 file_name.set_extension("json");
903
904 let expected_feedback =
905 serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
906
907 assert_eq!(expected_feedback, feedback);
908
909 }
915 }
916
917 #[test]
918 fn dmarc_report_eml_parse() {
919 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
920 test_dir.push("resources");
921 test_dir.push("dmarc-feedback");
922
923 for file_name in fs::read_dir(&test_dir).unwrap() {
924 let mut file_name = file_name.unwrap().path();
925 if !file_name.extension().unwrap().to_str().unwrap().eq("eml") {
926 continue;
927 }
928 println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
929
930 let feedback = Report::parse_rfc5322(&fs::read(&file_name).unwrap()).unwrap();
931
932 file_name.set_extension("json");
933
934 let expected_feedback =
935 serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
936
937 assert_eq!(expected_feedback, feedback);
938
939 }
945 }
946}