1pub mod arf;
8pub mod dmarc;
9pub mod tlsrpt;
10use serde::{Deserialize, Serialize};
11use std::{borrow::Cow, io::Read, net::IpAddr};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[cfg_attr(
15 feature = "rkyv",
16 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
17)]
18pub struct DateRange {
19 pub begin: u64,
20 pub end: u64,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
24#[cfg_attr(
25 feature = "rkyv",
26 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
27)]
28pub struct ReportMetadata {
29 pub org_name: String,
30 pub email: String,
31 pub extra_contact_info: Option<String>,
32 pub report_id: String,
33 pub date_range: DateRange,
34 pub error: Vec<String>,
35 #[serde(default)]
36 pub generator: Option<String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
40#[cfg_attr(
41 feature = "rkyv",
42 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
43)]
44pub enum Alignment {
45 Relaxed,
46 Strict,
47 #[default]
48 Unspecified,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
52#[cfg_attr(
53 feature = "rkyv",
54 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
55)]
56pub enum Disposition {
57 None,
58 Quarantine,
59 Reject,
60 #[default]
61 Unspecified,
62}
63
64#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
65#[cfg_attr(
66 feature = "rkyv",
67 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
68)]
69pub enum ActionDisposition {
70 None,
71 Pass,
72 Quarantine,
73 Reject,
74 #[default]
75 Unspecified,
76}
77
78#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
79#[cfg_attr(
80 feature = "rkyv",
81 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
82)]
83pub struct PolicyPublished {
84 pub domain: String,
85 pub version_published: Option<f32>,
86 pub adkim: Alignment,
87 pub aspf: Alignment,
88 pub p: Disposition,
89 pub sp: Disposition,
90 #[serde(default)]
91 pub np: Disposition,
92 pub testing: bool,
93 #[serde(default)]
94 pub discovery_method: Discovery,
95 pub fo: Option<String>,
96}
97
98impl Eq for PolicyPublished {}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
101#[cfg_attr(
102 feature = "rkyv",
103 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
104)]
105pub enum Discovery {
106 Psl,
107 Treewalk,
108 #[default]
109 Unspecified,
110}
111
112#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
113#[cfg_attr(
114 feature = "rkyv",
115 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
116)]
117pub enum DmarcResult {
118 Pass,
119 Fail,
120 #[default]
121 Unspecified,
122}
123
124#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
125#[cfg_attr(
126 feature = "rkyv",
127 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
128)]
129pub enum PolicyOverride {
130 LocalPolicy,
131 MailingList,
132 PolicyTestMode,
133 TrustedForwarder,
134 #[default]
135 Other,
136}
137
138#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
139#[cfg_attr(
140 feature = "rkyv",
141 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
142)]
143pub struct PolicyOverrideReason {
144 pub type_: PolicyOverride,
145 pub comment: Option<String>,
146}
147
148#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
149#[cfg_attr(
150 feature = "rkyv",
151 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
152)]
153pub struct PolicyEvaluated {
154 pub disposition: ActionDisposition,
155 pub dkim: DmarcResult,
156 pub spf: DmarcResult,
157 pub reason: Vec<PolicyOverrideReason>,
158}
159
160#[derive(Debug, Clone, Hash, Default, PartialEq, Eq, Serialize, Deserialize)]
161#[cfg_attr(
162 feature = "rkyv",
163 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
164)]
165pub struct Row {
166 pub source_ip: Option<IpAddr>,
167 pub count: u32,
168 pub policy_evaluated: PolicyEvaluated,
169}
170
171#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
172#[cfg_attr(
173 feature = "rkyv",
174 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
175)]
176pub struct Extension {
177 pub name: String,
178 pub definition: String,
179}
180
181#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
182#[cfg_attr(
183 feature = "rkyv",
184 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
185)]
186pub struct Identifier {
187 pub envelope_to: Option<String>,
188 pub envelope_from: String,
189 pub header_from: String,
190}
191
192#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
193#[cfg_attr(
194 feature = "rkyv",
195 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
196)]
197pub enum DkimResult {
198 #[default]
199 None,
200 Pass,
201 Fail,
202 Policy,
203 Neutral,
204 TempError,
205 PermError,
206}
207
208#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
209#[cfg_attr(
210 feature = "rkyv",
211 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
212)]
213pub struct DKIMAuthResult {
214 pub domain: String,
215 pub selector: String,
216 pub result: DkimResult,
217 pub human_result: Option<String>,
218}
219
220#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
221#[cfg_attr(
222 feature = "rkyv",
223 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
224)]
225pub enum SPFDomainScope {
226 Helo,
227 MailFrom,
228 #[default]
229 Unspecified,
230}
231
232#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
233#[cfg_attr(
234 feature = "rkyv",
235 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
236)]
237pub enum SpfResult {
238 #[default]
239 None,
240 Neutral,
241 Pass,
242 Fail,
243 SoftFail,
244 TempError,
245 PermError,
246}
247
248#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
249#[cfg_attr(
250 feature = "rkyv",
251 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
252)]
253pub struct SPFAuthResult {
254 pub domain: String,
255 pub scope: SPFDomainScope,
256 pub result: SpfResult,
257 pub human_result: Option<String>,
258}
259
260#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
261#[cfg_attr(
262 feature = "rkyv",
263 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
264)]
265pub struct AuthResult {
266 pub dkim: Vec<DKIMAuthResult>,
267 pub spf: Vec<SPFAuthResult>,
268}
269
270#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
271#[cfg_attr(
272 feature = "rkyv",
273 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
274)]
275pub struct Record {
276 pub row: Row,
277 pub identifiers: Identifier,
278 pub auth_results: AuthResult,
279 pub extensions: Vec<Extension>,
280}
281
282#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
283#[cfg_attr(
284 feature = "rkyv",
285 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
286)]
287pub struct Report {
288 pub version: f32,
289 pub report_metadata: ReportMetadata,
290 pub policy_published: PolicyPublished,
291 pub record: Vec<Record>,
292 pub extensions: Vec<Extension>,
293}
294
295impl Eq for Report {}
296
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub enum Error {
299 MailParseError,
300 ReportParseError(String),
301 UncompressError(String),
302 ReportTooLarge,
303 NoReportsFound,
304}
305
306const MAX_SIZE_RESERVATION: u64 = 64 * 1024;
307
308pub(crate) fn read_capped(
309 reader: impl Read,
310 size_hint: u64,
311 max_size: usize,
312) -> Result<Vec<u8>, Error> {
313 let max_size = max_size as u64;
314 if size_hint > max_size {
315 return Err(Error::ReportTooLarge);
316 }
317
318 let mut buf = Vec::with_capacity(size_hint.min(MAX_SIZE_RESERVATION) as usize);
319 reader
320 .take(max_size.saturating_add(1))
321 .read_to_end(&mut buf)
322 .map_err(|err| Error::UncompressError(err.to_string()))?;
323
324 if buf.len() as u64 > max_size {
325 return Err(Error::ReportTooLarge);
326 }
327
328 Ok(buf)
329}
330
331impl From<String> for Error {
332 fn from(err: String) -> Self {
333 Error::ReportParseError(err)
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
338#[cfg_attr(
339 feature = "rkyv",
340 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
341)]
342pub struct Feedback<'x> {
343 pub feedback_type: FeedbackType,
344 pub arrival_date: Option<i64>,
345 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
346 pub authentication_results: Vec<Cow<'x, str>>,
347 pub incidents: u32,
348 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
349 pub original_envelope_id: Option<Cow<'x, str>>,
350 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
351 pub original_mail_from: Option<Cow<'x, str>>,
352 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
353 pub original_rcpt_to: Option<Cow<'x, str>>,
354 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
355 pub reported_domain: Vec<Cow<'x, str>>,
356 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
357 pub reported_uri: Vec<Cow<'x, str>>,
358 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
359 pub reporting_mta: Option<Cow<'x, str>>,
360 pub source_ip: Option<IpAddr>,
361 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
362 pub user_agent: Option<Cow<'x, str>>,
363 pub version: u32,
364 pub source_port: u32,
365
366 pub auth_failure: AuthFailureType,
368 pub delivery_result: DeliveryResult,
369 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
370 pub dkim_adsp_dns: Option<Cow<'x, str>>,
371 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
372 pub dkim_canonicalized_body: Option<Cow<'x, str>>,
373 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
374 pub dkim_canonicalized_header: Option<Cow<'x, str>>,
375 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
376 pub dkim_domain: Option<Cow<'x, str>>,
377 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
378 pub dkim_identity: Option<Cow<'x, str>>,
379 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
380 pub dkim_selector: Option<Cow<'x, str>>,
381 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
382 pub dkim_selector_dns: Option<Cow<'x, str>>,
383 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
384 pub spf_dns: Option<Cow<'x, str>>,
385 pub identity_alignment: IdentityAlignment,
386
387 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
388 pub message: Option<Cow<'x, str>>,
389 #[cfg_attr(feature = "rkyv", rkyv(with = rkyv::with::Map<rkyv::with::AsOwned>))]
390 pub headers: Option<Cow<'x, str>>,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
394#[cfg_attr(
395 feature = "rkyv",
396 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
397)]
398pub enum AuthFailureType {
399 Adsp,
400 BodyHash,
401 Revoked,
402 Signature,
403 Spf,
404 Dmarc,
405 #[default]
406 Unspecified,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
410#[cfg_attr(
411 feature = "rkyv",
412 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
413)]
414pub enum IdentityAlignment {
415 None,
416 Spf,
417 Dkim,
418 DkimSpf,
419 #[default]
420 Unspecified,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
424#[cfg_attr(
425 feature = "rkyv",
426 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
427)]
428pub enum DeliveryResult {
429 Delivered,
430 Spam,
431 Policy,
432 Reject,
433 Other,
434 #[default]
435 Unspecified,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Copy, Serialize, Deserialize, Default)]
439#[cfg_attr(
440 feature = "rkyv",
441 derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
442)]
443pub enum FeedbackType {
444 Abuse,
445 AuthFailure,
446 Fraud,
447 NotSpam,
448 #[default]
449 Other,
450 Virus,
451}
452
453impl From<&crate::DkimResult> for AuthFailureType {
454 fn from(value: &crate::DkimResult) -> Self {
455 match value {
456 crate::DkimResult::Neutral(err)
457 | crate::DkimResult::Fail(err)
458 | crate::DkimResult::PermError(err)
459 | crate::DkimResult::TempError(err) => match err {
460 crate::Error::Dkim(crate::dkim::DkimError::FailedBodyHashMatch) => {
461 AuthFailureType::BodyHash
462 }
463 #[cfg(feature = "arc")]
464 crate::Error::Arc(crate::arc::ArcError::FailedBodyHashMatch) => {
465 AuthFailureType::BodyHash
466 }
467 crate::Error::Dkim(crate::dkim::DkimError::RevokedPublicKey) => {
468 AuthFailureType::Revoked
469 }
470 _ => AuthFailureType::Signature,
471 },
472 crate::DkimResult::Pass | crate::DkimResult::None => AuthFailureType::Signature,
473 }
474 }
475}
476
477#[cfg(test)]
478mod test {
479 const MAX_REPORT_SIZE: usize = 25 * 1024 * 1024;
480 use super::{Error, read_capped, test_util::gzip};
481
482 #[test]
483 fn read_capped_rejects_forged_size_hint() {
484 assert_eq!(
485 read_capped(&b"hello"[..], u32::MAX as u64, MAX_REPORT_SIZE),
486 Err(Error::ReportTooLarge)
487 );
488 assert_eq!(
489 read_capped(&b"hello"[..], u64::MAX, MAX_REPORT_SIZE),
490 Err(Error::ReportTooLarge)
491 );
492 }
493
494 #[test]
495 fn read_capped_rejects_oversized_output() {
496 assert_eq!(
497 read_capped(&[0u8; 1024][..], 0, 1023),
498 Err(Error::ReportTooLarge)
499 );
500 assert_eq!(read_capped(&b"hello"[..], 0, 0), Err(Error::ReportTooLarge));
501 }
502
503 #[test]
504 fn read_capped_accepts_within_limit() {
505 assert_eq!(read_capped(&b"hello"[..], 5, 5), Ok(b"hello".to_vec()));
506 assert_eq!(read_capped(&b""[..], 0, 0), Ok(Vec::new()));
507 assert_eq!(
508 read_capped(&b"hello"[..], u32::MAX as u64, u32::MAX as usize),
509 Ok(b"hello".to_vec())
510 );
511 }
512
513 #[test]
514 fn read_capped_bounds_decompressed_output() {
515 let bomb = gzip(&vec![b'a'; 1024 * 1024]);
516 assert!(bomb.len() < 8192);
517
518 assert_eq!(
519 read_capped(flate2::read::GzDecoder::new(&bomb[..]), 0, 64 * 1024),
520 Err(Error::ReportTooLarge)
521 );
522 assert_eq!(
523 read_capped(flate2::read::GzDecoder::new(&bomb[..]), 0, MAX_REPORT_SIZE)
524 .map(|buf| buf.len()),
525 Ok(1024 * 1024)
526 );
527 }
528}
529
530#[cfg(test)]
531pub(crate) mod test_util {
532 use flate2::{Compression, Crc, write::GzEncoder};
533 use mail_builder::{
534 MessageBuilder,
535 mime::{BodyPart, MimePart},
536 };
537 use std::io::Write;
538
539 pub(crate) fn gzip(data: &[u8]) -> Vec<u8> {
540 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
541 encoder.write_all(data).unwrap();
542 encoder.finish().unwrap()
543 }
544
545 pub(crate) fn zip(
546 name: &str,
547 data: &[u8],
548 compressed_size: Option<u32>,
549 uncompressed_size: Option<u32>,
550 ) -> Vec<u8> {
551 let mut crc = Crc::new();
552 crc.update(data);
553 let crc = crc.sum();
554 let compressed_size = compressed_size.unwrap_or(data.len() as u32);
555 let uncompressed_size = uncompressed_size.unwrap_or(data.len() as u32);
556 let name = name.as_bytes();
557
558 let mut out = Vec::new();
559 out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
560 out.extend_from_slice(&20u16.to_le_bytes());
561 out.extend_from_slice(&0u16.to_le_bytes());
562 out.extend_from_slice(&0u16.to_le_bytes());
563 out.extend_from_slice(&0u16.to_le_bytes());
564 out.extend_from_slice(&0u16.to_le_bytes());
565 out.extend_from_slice(&crc.to_le_bytes());
566 out.extend_from_slice(&compressed_size.to_le_bytes());
567 out.extend_from_slice(&uncompressed_size.to_le_bytes());
568 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
569 out.extend_from_slice(&0u16.to_le_bytes());
570 out.extend_from_slice(name);
571 out.extend_from_slice(data);
572
573 let central_offset = out.len() as u32;
574 out.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
575 out.extend_from_slice(&20u16.to_le_bytes());
576 out.extend_from_slice(&20u16.to_le_bytes());
577 out.extend_from_slice(&0u16.to_le_bytes());
578 out.extend_from_slice(&0u16.to_le_bytes());
579 out.extend_from_slice(&0u16.to_le_bytes());
580 out.extend_from_slice(&0u16.to_le_bytes());
581 out.extend_from_slice(&crc.to_le_bytes());
582 out.extend_from_slice(&compressed_size.to_le_bytes());
583 out.extend_from_slice(&uncompressed_size.to_le_bytes());
584 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
585 out.extend_from_slice(&0u16.to_le_bytes());
586 out.extend_from_slice(&0u16.to_le_bytes());
587 out.extend_from_slice(&0u16.to_le_bytes());
588 out.extend_from_slice(&0u16.to_le_bytes());
589 out.extend_from_slice(&0u32.to_le_bytes());
590 out.extend_from_slice(&0u32.to_le_bytes());
591 out.extend_from_slice(name);
592
593 let central_size = out.len() as u32 - central_offset;
594 out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
595 out.extend_from_slice(&0u16.to_le_bytes());
596 out.extend_from_slice(&0u16.to_le_bytes());
597 out.extend_from_slice(&1u16.to_le_bytes());
598 out.extend_from_slice(&1u16.to_le_bytes());
599 out.extend_from_slice(¢ral_size.to_le_bytes());
600 out.extend_from_slice(¢ral_offset.to_le_bytes());
601 out.extend_from_slice(&0u16.to_le_bytes());
602
603 out
604 }
605
606 pub(crate) fn message_with_attachment(
607 content_type: &str,
608 file_name: &str,
609 contents: &[u8],
610 ) -> Vec<u8> {
611 MessageBuilder::new()
612 .from(("Mail Delivery System", "postmaster@example.org"))
613 .to("postmaster@example.com")
614 .subject("Report Domain: example.com")
615 .body(MimePart::new(
616 "multipart/report",
617 BodyPart::Multipart(vec![
618 MimePart::new("text/plain", BodyPart::Text("Report attached.".into())),
619 MimePart::new(content_type, BodyPart::Binary(contents.into()))
620 .attachment(file_name),
621 ]),
622 ))
623 .write_to_vec()
624 .unwrap()
625 }
626}