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, read_capped,
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};
20use std::net::IpAddr;
21use std::str::FromStr;
22
23impl Report {
24 pub fn parse_rfc5322(report: &[u8], max_size: usize) -> 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 buf = read_capped(GzDecoder::new(report), 0, max_size)?;
85
86 match Report::parse_xml(&buf) {
87 Ok(feedback) => return Ok(feedback),
88 Err(err) => {
89 error = err.into();
90 }
91 }
92 }
93 ReportType::Zip => {
94 let mut archive = zip::ZipArchive::new(Cursor::new(report))
95 .map_err(|err| Error::UncompressError(err.to_string()))?;
96 for i in 0..archive.len() {
97 match archive.by_index(i) {
98 Ok(mut file) => {
99 let size_hint = file.size();
100 let buf = read_capped(&mut file, size_hint, max_size)?;
101 match Report::parse_xml(&buf) {
102 Ok(feedback) => return Ok(feedback),
103 Err(err) => {
104 error = err.into();
105 }
106 }
107 }
108 Err(err) => {
109 error = Error::UncompressError(err.to_string());
110 }
111 }
112 }
113 }
114 ReportType::Xml => match Report::parse_xml(report) {
115 Ok(feedback) => return Ok(feedback),
116 Err(err) => {
117 error = err.into();
118 }
119 },
120 }
121 }
122 _ => (),
123 }
124 }
125
126 Err(error)
127 }
128
129 pub fn parse_xml(report: &[u8]) -> Result<Self, String> {
130 let mut version: f32 = 0.0;
131 let mut report_metadata = None;
132 let mut policy_published = None;
133 let mut record = Vec::new();
134 let mut extensions = Vec::new();
135
136 let mut reader = Reader::from_reader(report);
137 reader.config_mut().trim_text(true);
138
139 let mut buf = Vec::with_capacity(128);
140 let mut found_feedback = false;
141
142 while let Some(tag) = reader.next_tag(&mut buf)? {
143 let name = tag.name();
144 if found_feedback {
145 hashify::fnc_map!(name.as_ref(),
146 b"version" => {
147 version = reader.next_value(&mut buf)?.unwrap_or(0.0);
148 },
149 b"report_metadata" => {
150 report_metadata = ReportMetadata::parse(&mut reader, &mut buf)?.into();
151 },
152 b"policy_published" => {
153 policy_published = PolicyPublished::parse(&mut reader, &mut buf)?.into();
154 },
155 b"record" => {
156 record.push(Record::parse(&mut reader, &mut buf)?);
157 },
158 b"extensions" => {
159 Extension::parse(&mut reader, &mut buf, &mut extensions)?;
160 },
161 _ => ()
162 );
163 } else if name.as_ref() == b"feedback" {
164 found_feedback = true;
165 } else if !name.as_ref().is_empty() {
166 return Err(format!(
167 "Unexpected tag {} at position {}.",
168 String::from_utf8_lossy(name.as_ref()),
169 reader.buffer_position()
170 ));
171 }
172 }
173
174 Ok(Report {
175 version,
176 report_metadata: report_metadata.ok_or("Missing feedback/report_metadata tag.")?,
177 policy_published: policy_published.ok_or("Missing feedback/policy_published tag.")?,
178 record,
179 extensions,
180 })
181 }
182}
183
184impl ReportMetadata {
185 pub(crate) fn parse<R: BufRead>(
186 reader: &mut Reader<R>,
187 buf: &mut Vec<u8>,
188 ) -> Result<Self, String> {
189 let mut rm = ReportMetadata::default();
190
191 while let Some(tag) = reader.next_tag(buf)? {
192 let name = tag.name();
193 hashify::fnc_map!(name.as_ref(),
194 b"org_name" => {
195 rm.org_name = reader.next_value::<String>(buf)?.unwrap_or_default();
196 },
197 b"email" => {
198 rm.email = reader.next_value::<String>(buf)?.unwrap_or_default();
199 },
200 b"extra_contact_info" => {
201 rm.extra_contact_info = reader.next_value::<String>(buf)?;
202 },
203 b"report_id" => {
204 rm.report_id = reader.next_value::<String>(buf)?.unwrap_or_default();
205 },
206 b"date_range" => {
207 rm.date_range = DateRange::parse(reader, buf)?;
208 },
209 b"error" => {
210 if let Some(err) = reader.next_value::<String>(buf)? {
211 rm.error.push(err);
212 }
213 },
214 b"generator" => {
215 rm.generator = reader.next_value::<String>(buf)?;
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 let name = tag.name();
237 hashify::fnc_map!(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 let name = tag.name();
264 hashify::fnc_map!(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"np" => {
284 p.np = reader.next_value(buf)?.unwrap_or_default();
285 },
286 b"discovery_method" => {
287 p.discovery_method = reader.next_value(buf)?.unwrap_or_default();
288 },
289 b"testing" => {
290 p.testing = reader
291 .next_value::<String>(buf)?
292 .is_some_and(|s| s.eq_ignore_ascii_case("y"));
293 },
294 b"fo" => {
295 p.fo = reader.next_value::<String>(buf)?;
296 },
297 b"" => (),
298 _ => {
299 reader.skip_tag(buf)?;
300 }
301 );
302 }
303
304 Ok(p)
305 }
306}
307
308impl Extension {
309 pub(crate) fn parse<R: BufRead>(
310 reader: &mut Reader<R>,
311 buf: &mut Vec<u8>,
312 extensions: &mut Vec<Extension>,
313 ) -> Result<(), String> {
314 let decoder = reader.decoder();
315 while let Some(tag) = reader.next_tag(buf)? {
316 let name = tag.name();
317 hashify::fnc_map!(name.as_ref(),
318 b"extension" => {
319 let mut e = Extension::default();
320 if let Ok(Some(attr)) = tag.try_get_attribute("name")
321 && let Ok(attr) =
322 attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
323 {
324 e.name = attr.to_string();
325 }
326 if let Ok(Some(attr)) = tag.try_get_attribute("definition")
327 && let Ok(attr) =
328 attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
329 {
330 e.definition = attr.to_string();
331 }
332 extensions.push(e);
333 reader.skip_tag(buf)?;
334 },
335 b"" => (),
336 _ => {
337 reader.skip_tag(buf)?;
338 }
339 );
340 }
341
342 Ok(())
343 }
344}
345
346impl Record {
347 pub(crate) fn parse<R: BufRead>(
348 reader: &mut Reader<R>,
349 buf: &mut Vec<u8>,
350 ) -> Result<Self, String> {
351 let mut r = Record::default();
352
353 while let Some(tag) = reader.next_tag(buf)? {
354 let name = tag.name();
355 hashify::fnc_map!(name.as_ref(),
356 b"row" => {
357 r.row = Row::parse(reader, buf)?;
358 },
359 b"identifiers" => {
360 r.identifiers = Identifier::parse(reader, buf)?;
361 },
362 b"auth_results" => {
363 r.auth_results = AuthResult::parse(reader, buf)?;
364 },
365 b"extensions" => {
366 Extension::parse(reader, buf, &mut r.extensions)?;
367 },
368 b"" => (),
369 _ => {
370 reader.skip_tag(buf)?;
371 }
372 );
373 }
374
375 Ok(r)
376 }
377}
378
379impl Row {
380 pub(crate) fn parse<R: BufRead>(
381 reader: &mut Reader<R>,
382 buf: &mut Vec<u8>,
383 ) -> Result<Self, String> {
384 let mut r = Row::default();
385
386 while let Some(tag) = reader.next_tag(buf)? {
387 let name = tag.name();
388 hashify::fnc_map!(name.as_ref(),
389 b"source_ip" => {
390 if let Some(ip) = reader.next_value::<IpAddr>(buf)? {
391 r.source_ip = ip.into();
392 }
393 },
394 b"count" => {
395 r.count = reader.next_value(buf)?.unwrap_or_default();
396 },
397 b"policy_evaluated" => {
398 r.policy_evaluated = PolicyEvaluated::parse(reader, buf)?;
399 },
400 b"" => (),
401 _ => {
402 reader.skip_tag(buf)?;
403 }
404 );
405 }
406
407 Ok(r)
408 }
409}
410
411impl PolicyEvaluated {
412 pub(crate) fn parse<R: BufRead>(
413 reader: &mut Reader<R>,
414 buf: &mut Vec<u8>,
415 ) -> Result<Self, String> {
416 let mut pe = PolicyEvaluated::default();
417
418 while let Some(tag) = reader.next_tag(buf)? {
419 let name = tag.name();
420 hashify::fnc_map!(name.as_ref(),
421 b"disposition" => {
422 pe.disposition = reader.next_value(buf)?.unwrap_or_default();
423 },
424 b"dkim" => {
425 pe.dkim = reader.next_value(buf)?.unwrap_or_default();
426 },
427 b"spf" => {
428 pe.spf = reader.next_value(buf)?.unwrap_or_default();
429 },
430 b"reason" => {
431 pe.reason.push(PolicyOverrideReason::parse(reader, buf)?);
432 },
433 b"" => (),
434 _ => {
435 reader.skip_tag(buf)?;
436 }
437 );
438 }
439
440 Ok(pe)
441 }
442}
443
444impl PolicyOverrideReason {
445 pub(crate) fn parse<R: BufRead>(
446 reader: &mut Reader<R>,
447 buf: &mut Vec<u8>,
448 ) -> Result<Self, String> {
449 let mut por = PolicyOverrideReason::default();
450
451 while let Some(tag) = reader.next_tag(buf)? {
452 let name = tag.name();
453 hashify::fnc_map!(name.as_ref(),
454 b"type" => {
455 por.type_ = reader.next_value(buf)?.unwrap_or_default();
456 },
457 b"comment" => {
458 por.comment = reader.next_value(buf)?;
459 },
460 b"" => (),
461 _ => {
462 reader.skip_tag(buf)?;
463 }
464 );
465 }
466
467 Ok(por)
468 }
469}
470
471impl Identifier {
472 pub(crate) fn parse<R: BufRead>(
473 reader: &mut Reader<R>,
474 buf: &mut Vec<u8>,
475 ) -> Result<Self, String> {
476 let mut i = Identifier::default();
477
478 while let Some(tag) = reader.next_tag(buf)? {
479 let name = tag.name();
480 hashify::fnc_map!(name.as_ref(),
481 b"envelope_to" => {
482 i.envelope_to = reader.next_value(buf)?;
483 },
484 b"envelope_from" => {
485 i.envelope_from = reader.next_value(buf)?.unwrap_or_default();
486 },
487 b"header_from" => {
488 i.header_from = reader.next_value(buf)?.unwrap_or_default();
489 },
490 b"" => (),
491 _ => {
492 reader.skip_tag(buf)?;
493 }
494 );
495 }
496
497 Ok(i)
498 }
499}
500
501impl AuthResult {
502 pub(crate) fn parse<R: BufRead>(
503 reader: &mut Reader<R>,
504 buf: &mut Vec<u8>,
505 ) -> Result<Self, String> {
506 let mut ar = AuthResult::default();
507
508 while let Some(tag) = reader.next_tag(buf)? {
509 let name = tag.name();
510 hashify::fnc_map!(name.as_ref(),
511 b"dkim" => {
512 ar.dkim.push(DKIMAuthResult::parse(reader, buf)?);
513 },
514 b"spf" => {
515 ar.spf.push(SPFAuthResult::parse(reader, buf)?);
516 },
517 b"" => (),
518 _ => {
519 reader.skip_tag(buf)?;
520 }
521 );
522 }
523
524 Ok(ar)
525 }
526}
527
528impl DKIMAuthResult {
529 pub(crate) fn parse<R: BufRead>(
530 reader: &mut Reader<R>,
531 buf: &mut Vec<u8>,
532 ) -> Result<Self, String> {
533 let mut dar = DKIMAuthResult::default();
534
535 while let Some(tag) = reader.next_tag(buf)? {
536 let name = tag.name();
537 hashify::fnc_map!(name.as_ref(),
538 b"domain" => {
539 dar.domain = reader.next_value(buf)?.unwrap_or_default();
540 },
541 b"selector" => {
542 dar.selector = reader.next_value(buf)?.unwrap_or_default();
543 },
544 b"result" => {
545 dar.result = reader.next_value(buf)?.unwrap_or_default();
546 },
547 b"human_result" => {
548 dar.human_result = reader.next_value(buf)?;
549 },
550 b"" => (),
551 _ => {
552 reader.skip_tag(buf)?;
553 }
554 );
555 }
556
557 Ok(dar)
558 }
559}
560
561impl SPFAuthResult {
562 pub(crate) fn parse<R: BufRead>(
563 reader: &mut Reader<R>,
564 buf: &mut Vec<u8>,
565 ) -> Result<Self, String> {
566 let mut sar = SPFAuthResult::default();
567
568 while let Some(tag) = reader.next_tag(buf)? {
569 let name = tag.name();
570 hashify::fnc_map!(name.as_ref(),
571 b"domain" => {
572 sar.domain = reader.next_value(buf)?.unwrap_or_default();
573 },
574 b"scope" => {
575 sar.scope = reader.next_value(buf)?.unwrap_or_default();
576 },
577 b"result" => {
578 sar.result = reader.next_value(buf)?.unwrap_or_default();
579 },
580 b"human_result" => {
581 sar.human_result = reader.next_value(buf)?;
582 },
583 b"" => (),
584 _ => {
585 reader.skip_tag(buf)?;
586 }
587 );
588 }
589
590 Ok(sar)
591 }
592}
593
594impl FromStr for PolicyOverride {
595 type Err = ();
596
597 fn from_str(s: &str) -> Result<Self, Self::Err> {
598 Ok(hashify::tiny_map!(s.as_bytes(),
599 b"trusted_forwarder" => PolicyOverride::TrustedForwarder,
600 b"mailing_list" => PolicyOverride::MailingList,
601 b"local_policy" => PolicyOverride::LocalPolicy,
602 b"policy_test_mode" => PolicyOverride::PolicyTestMode,
603 )
604 .unwrap_or(PolicyOverride::Other))
605 }
606}
607
608impl FromStr for Discovery {
609 type Err = ();
610
611 fn from_str(s: &str) -> Result<Self, Self::Err> {
612 Ok(hashify::tiny_map!(s.as_bytes(),
613 b"psl" => Discovery::Psl,
614 b"treewalk" => Discovery::Treewalk,
615 )
616 .unwrap_or(Discovery::Unspecified))
617 }
618}
619
620impl FromStr for DmarcResult {
621 type Err = ();
622
623 fn from_str(s: &str) -> Result<Self, Self::Err> {
624 Ok(hashify::tiny_map!(s.as_bytes(),
625 b"pass" => DmarcResult::Pass,
626 b"fail" => DmarcResult::Fail,
627 )
628 .unwrap_or(DmarcResult::Unspecified))
629 }
630}
631
632impl FromStr for DkimResult {
633 type Err = ();
634
635 fn from_str(s: &str) -> Result<Self, Self::Err> {
636 Ok(hashify::tiny_map!(s.as_bytes(),
637 b"none" => DkimResult::None,
638 b"pass" => DkimResult::Pass,
639 b"fail" => DkimResult::Fail,
640 b"policy" => DkimResult::Policy,
641 b"neutral" => DkimResult::Neutral,
642 b"temperror" => DkimResult::TempError,
643 b"permerror" => DkimResult::PermError,
644 )
645 .unwrap_or(DkimResult::None))
646 }
647}
648
649impl FromStr for SpfResult {
650 type Err = ();
651
652 fn from_str(s: &str) -> Result<Self, Self::Err> {
653 Ok(hashify::tiny_map!(s.as_bytes(),
654 b"none" => SpfResult::None,
655 b"pass" => SpfResult::Pass,
656 b"fail" => SpfResult::Fail,
657 b"softfail" => SpfResult::SoftFail,
658 b"neutral" => SpfResult::Neutral,
659 b"temperror" => SpfResult::TempError,
660 b"permerror" => SpfResult::PermError,
661 )
662 .unwrap_or(SpfResult::None))
663 }
664}
665
666impl FromStr for SPFDomainScope {
667 type Err = ();
668
669 fn from_str(s: &str) -> Result<Self, Self::Err> {
670 Ok(hashify::tiny_map!(s.as_bytes(),
671 b"helo" => SPFDomainScope::Helo,
672 b"mfrom" => SPFDomainScope::MailFrom,
673 )
674 .unwrap_or(SPFDomainScope::Unspecified))
675 }
676}
677
678impl FromStr for ActionDisposition {
679 type Err = ();
680
681 fn from_str(s: &str) -> Result<Self, Self::Err> {
682 Ok(hashify::tiny_map!(s.as_bytes(),
683 b"none" => ActionDisposition::None,
684 b"pass" => ActionDisposition::Pass,
685 b"quarantine" => ActionDisposition::Quarantine,
686 b"reject" => ActionDisposition::Reject,
687 )
688 .unwrap_or(ActionDisposition::Unspecified))
689 }
690}
691
692impl FromStr for Disposition {
693 type Err = ();
694
695 fn from_str(s: &str) -> Result<Self, Self::Err> {
696 Ok(hashify::tiny_map!(s.as_bytes(),
697 b"none" => Disposition::None,
698 b"quarantine" => Disposition::Quarantine,
699 b"reject" => Disposition::Reject,
700 )
701 .unwrap_or(Disposition::Unspecified))
702 }
703}
704
705impl FromStr for Alignment {
706 type Err = ();
707
708 fn from_str(s: &str) -> Result<Self, Self::Err> {
709 Ok(match s.as_bytes().first() {
710 Some(b'r') => Alignment::Relaxed,
711 Some(b's') => Alignment::Strict,
712 _ => Alignment::Unspecified,
713 })
714 }
715}
716
717trait ReaderHelper {
718 fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>;
719 fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>;
720 fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>;
721}
722
723impl<R: BufRead> ReaderHelper for Reader<R> {
724 fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> {
725 match self.read_event_into(buf) {
726 Ok(Event::Start(e)) => Ok(Some(e)),
727 Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None),
728 Err(e) => Err(format!(
729 "Error at position {}: {:?}",
730 self.buffer_position(),
731 e
732 )),
733 _ => Ok(Some(BytesStart::new(""))),
734 }
735 }
736
737 fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> {
738 let mut value: Option<String> = None;
739
740 loop {
741 match self.read_event_into(buf) {
742 Ok(Event::Text(e)) => {
743 let v = e.xml_content(XmlVersion::Implicit1_0).map_err(|e| {
744 format!(
745 "Failed to decode text value at position {}: {}",
746 self.buffer_position(),
747 e
748 )
749 })?;
750 if let Some(value) = &mut value {
751 value.push_str(&v);
752 } else {
753 value = Some(v.into_owned());
754 }
755 }
756 Ok(Event::GeneralRef(e)) => {
757 let v = hashify::tiny_map!(&*e,
758 b"lt" => "<",
759 b"gt" => ">",
760 b"amp" => "&",
761 b"apos" => "'",
762 b"quot" => "\"",
763 )
764 .map(Cow::Borrowed)
765 .or_else(|| {
766 e.resolve_char_ref()
767 .ok()
768 .flatten()
769 .map(|v| Cow::Owned(v.to_string()))
770 })
771 .unwrap_or_else(|| e.xml_content(XmlVersion::Implicit1_0).unwrap_or_default());
772
773 if let Some(value) = &mut value {
774 value.push_str(&v);
775 } else {
776 value = Some(v.into_owned());
777 }
778 }
779 Ok(Event::End(_)) => {
780 break;
781 }
782 Ok(Event::Start(e)) => {
783 return Err(format!(
784 "Expected value, found unexpected tag {} at position {}.",
785 String::from_utf8_lossy(e.name().as_ref()),
786 self.buffer_position()
787 ));
788 }
789 Ok(Event::Eof) => {
790 return Err(format!(
791 "Expected value, found unexpected EOF at position {}.",
792 self.buffer_position()
793 ));
794 }
795 _ => (),
796 }
797 }
798
799 Ok(value.and_then(|v| T::from_str(&v).ok()))
800 }
801
802 fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> {
803 let mut tag_count = 0;
804 loop {
805 match self.read_event_into(buf) {
806 Ok(Event::End(_)) => {
807 if tag_count == 0 {
808 break;
809 } else {
810 tag_count -= 1;
811 }
812 }
813 Ok(Event::Start(_)) => {
814 tag_count += 1;
815 }
816 Ok(Event::Eof) => {
817 return Err(format!(
818 "Expected value, found unexpected EOF at position {}.",
819 self.buffer_position()
820 ));
821 }
822 _ => (),
823 }
824 }
825 Ok(())
826 }
827}
828
829#[cfg(test)]
830mod test {
831 use crate::report::{
832 Discovery, Disposition, Error, PolicyOverride, Report, SPFDomainScope,
833 test_util::{gzip, message_with_attachment, zip},
834 };
835 use std::{fs, path::PathBuf};
836 const MAX_REPORT_SIZE: usize = 25 * 1024 * 1024;
837
838 fn resource(name: &str) -> Vec<u8> {
839 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
840 path.push("resources");
841 path.push("dmarc-feedback");
842 path.push(name);
843 fs::read(path).unwrap()
844 }
845
846 const REPORT: &str = concat!(
847 r#"<?xml version="1.0" encoding="UTF-8"?><feedback><report_metadata>"#,
848 r#"<org_name>Example</org_name><email>dmarc@example.org</email>"#,
849 r#"<report_id>1</report_id><date_range><begin>1</begin><end>2</end></date_range>"#,
850 r#"</report_metadata><policy_published><domain>example.org</domain>"#,
851 r#"</policy_published></feedback>"#
852 );
853
854 #[test]
855 fn dmarc_report_rfc9990_sample() {
856 let report = Report::parse_xml(&resource("004.xml")).unwrap();
859 assert_eq!(report.domain(), "example.com");
860 assert_eq!(report.np(), Disposition::None);
861 assert_eq!(report.discovery_method(), Discovery::Treewalk);
862 assert_eq!(
863 report.generator(),
864 Some("Example DMARC Aggregate Reporter v1.2")
865 );
866 assert!(!report.testing());
867
868 let reparsed = Report::parse_xml(report.to_xml().as_bytes()).unwrap();
870 assert_eq!(report, reparsed);
871 }
872
873 #[test]
874 fn dmarc_report_rfc7489_backwards_compat() {
875 let report = Report::parse_xml(&resource("005.xml")).unwrap();
878 assert_eq!(report.domain(), "example.com");
879 assert_eq!(report.p(), Disposition::Reject);
880 assert_eq!(report.np(), Disposition::Unspecified);
881 assert_eq!(report.discovery_method(), Discovery::Unspecified);
882 assert_eq!(report.generator(), None);
883
884 let record = &report.records()[0];
885 assert_eq!(
886 record.policy_override_reason()[0].policy_override(),
887 PolicyOverride::Other
888 );
889 assert_eq!(record.spf_auth_result()[0].scope(), SPFDomainScope::Helo);
890 }
891
892 #[test]
893 fn dmarc_report_parse() {
894 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
895 test_dir.push("resources");
896 test_dir.push("dmarc-feedback");
897
898 for file_name in fs::read_dir(&test_dir).unwrap() {
899 let mut file_name = file_name.unwrap().path();
900 if !file_name.extension().unwrap().to_str().unwrap().eq("xml") {
901 continue;
902 }
903 println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
904
905 let feedback = Report::parse_xml(&fs::read(&file_name).unwrap()).unwrap();
906
907 file_name.set_extension("json");
908
909 let expected_feedback =
910 serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
911
912 assert_eq!(expected_feedback, feedback);
913
914 }
920 }
921
922 #[test]
923 fn dmarc_report_eml_parse() {
924 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
925 test_dir.push("resources");
926 test_dir.push("dmarc-feedback");
927
928 for file_name in fs::read_dir(&test_dir).unwrap() {
929 let mut file_name = file_name.unwrap().path();
930 if !file_name.extension().unwrap().to_str().unwrap().eq("eml") {
931 continue;
932 }
933 println!("Parsing DMARC feedback {}", file_name.to_str().unwrap());
934
935 let feedback =
936 Report::parse_rfc5322(&fs::read(&file_name).unwrap(), MAX_REPORT_SIZE).unwrap();
937
938 file_name.set_extension("json");
939
940 let expected_feedback =
941 serde_json::from_slice::<Report>(&fs::read(&file_name).unwrap()).unwrap();
942
943 assert_eq!(expected_feedback, feedback);
944
945 }
951 }
952
953 #[test]
954 fn dmarc_report_zip_forged_size() {
955 let archive = zip("report.xml", REPORT.as_bytes(), None, Some(u32::MAX));
956 let message = message_with_attachment("application/zip", "report.zip", &archive);
957
958 assert_eq!(
959 Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
960 Err(Error::ReportTooLarge)
961 );
962 }
963
964 #[test]
965 fn dmarc_report_zip_forged_compressed_size() {
966 let archive = zip("report.xml", REPORT.as_bytes(), Some(u32::MAX), None);
967 let message = message_with_attachment("application/zip", "report.zip", &archive);
968
969 assert!(Report::parse_rfc5322(&message, MAX_REPORT_SIZE).is_err());
970 }
971
972 #[test]
973 fn dmarc_report_zip_within_limit() {
974 let archive = zip("report.xml", REPORT.as_bytes(), None, None);
975 let message = message_with_attachment("application/zip", "report.zip", &archive);
976
977 assert_eq!(
978 Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
979 Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
980 );
981 assert_eq!(
982 Report::parse_rfc5322(&message, REPORT.len() - 1),
983 Err(Error::ReportTooLarge)
984 );
985 }
986
987 #[test]
988 fn dmarc_report_gzip_bomb() {
989 let bomb = gzip(&vec![b' '; 1024 * 1024]);
990 let message = message_with_attachment("application/gzip", "report.xml.gz", &bomb);
991
992 assert_eq!(
993 Report::parse_rfc5322(&message, 64 * 1024),
994 Err(Error::ReportTooLarge)
995 );
996 }
997
998 #[test]
999 fn dmarc_report_gzip_within_limit() {
1000 let message = message_with_attachment(
1001 "application/gzip",
1002 "report.xml.gz",
1003 &gzip(REPORT.as_bytes()),
1004 );
1005
1006 assert_eq!(
1007 Report::parse_rfc5322(&message, MAX_REPORT_SIZE),
1008 Ok(Report::parse_xml(REPORT.as_bytes()).unwrap())
1009 );
1010 assert_eq!(
1011 Report::parse_rfc5322(&message, REPORT.len() - 1),
1012 Err(Error::ReportTooLarge)
1013 );
1014 }
1015}