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