diff --git a/Cargo.lock b/Cargo.lock index 3aa2b1e6..87e8374d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2323,7 +2323,7 @@ dependencies = [ "bounce-classify", "chrono", "k9", - "mailparse", + "mailparsing", "rfc5321", "serde", "serde_json", diff --git a/crates/kumo-log-types/Cargo.toml b/crates/kumo-log-types/Cargo.toml index a4b10f8d..7e4e6c25 100644 --- a/crates/kumo-log-types/Cargo.toml +++ b/crates/kumo-log-types/Cargo.toml @@ -10,8 +10,8 @@ anyhow = "1.0" base64 = "0.13" bounce-classify = {path="../bounce-classify"} chrono = {version="0.4", default-features=false, features=["serde", "std"]} -mailparse = "0.14" #message = {path="../message"} +mailparsing = {path="../mailparsing"} rfc5321 = {path="../rfc5321"} serde = {version="1.0", features=["derive"]} serde_json = "1.0" diff --git a/crates/kumo-log-types/data/rfc3464/1.eml b/crates/kumo-log-types/data/rfc3464/1.eml index 54b98e86..5ce987d3 100644 --- a/crates/kumo-log-types/data/rfc3464/1.eml +++ b/crates/kumo-log-types/data/rfc3464/1.eml @@ -1,41 +1,41 @@ -Date: Thu, 7 Jul 1994 17:16:05 -0400 -From: Mail Delivery Subsystem -Message-Id: <199407072116.RAA14128@CS.UTK.EDU> -Subject: Returned mail: Cannot send message for 5 days -To: -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery- status; - boundary="RAA14128.773615765/CS.UTK.EDU" - ---RAA14128.773615765/CS.UTK.EDU - -The original message was received at Sat, 2 Jul 1994 17:10:28 -0400 -from root@localhost - - ----- The following addresses had delivery problems ----- - (unrecoverable error) - ------ Transcript of session follows ----- -... Deferred: Connection timed out - with larry.slip.umd.edu. -Message could not be delivered for 5 days -Message will be deleted from queue - ---RAA14128.773615765/CS.UTK.EDU -content-type: message/delivery-status - -Reporting-MTA: dns; cs.utk.edu - -Original-Recipient: rfc822;louisl@larry.slip.umd.edu -Final-Recipient: rfc822;louisl@larry.slip.umd.edu -Action: failed -Status: 4.0.0 -Diagnostic-Code: smtp; 426 connection timed out -Last-Attempt-Date: Thu, 7 Jul 1994 17:15:49 -0400 - ---RAA14128.773615765/CS.UTK.EDU -content-type: message/rfc822 - -[original message goes here] - ---RAA14128.773615765/CS.UTK.EDU-- +Date: Thu, 7 Jul 1994 17:16:05 -0400 +From: Mail Delivery Subsystem +Message-Id: <199407072116.RAA14128@CS.UTK.EDU> +Subject: Returned mail: Cannot send message for 5 days +To: +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="RAA14128.773615765/CS.UTK.EDU" + +--RAA14128.773615765/CS.UTK.EDU + +The original message was received at Sat, 2 Jul 1994 17:10:28 -0400 +from root@localhost + + ----- The following addresses had delivery problems ----- + (unrecoverable error) + +----- Transcript of session follows ----- +... Deferred: Connection timed out + with larry.slip.umd.edu. +Message could not be delivered for 5 days +Message will be deleted from queue + +--RAA14128.773615765/CS.UTK.EDU +content-type: message/delivery-status + +Reporting-MTA: dns; cs.utk.edu + +Original-Recipient: rfc822;louisl@larry.slip.umd.edu +Final-Recipient: rfc822;louisl@larry.slip.umd.edu +Action: failed +Status: 4.0.0 +Diagnostic-Code: smtp; 426 connection timed out +Last-Attempt-Date: Thu, 7 Jul 1994 17:15:49 -0400 + +--RAA14128.773615765/CS.UTK.EDU +content-type: message/rfc822 + +[original message goes here] + +--RAA14128.773615765/CS.UTK.EDU-- diff --git a/crates/kumo-log-types/src/rfc3464.rs b/crates/kumo-log-types/src/rfc3464.rs index 7fc39e0d..43718d36 100644 --- a/crates/kumo-log-types/src/rfc3464.rs +++ b/crates/kumo-log-types/src/rfc3464.rs @@ -5,7 +5,7 @@ use crate::rfc5965::{ }; use anyhow::{anyhow, Context}; use chrono::{DateTime, Utc}; -use mailparse::{parse_headers, parse_mail, ParsedMail}; +use mailparsing::MimePart; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; @@ -226,29 +226,38 @@ pub struct Report { pub original_message: Option, } +pub(crate) fn content_type(part: &MimePart) -> Option { + let ct = part.headers().content_type().ok()??; + Some(ct.value) +} + impl Report { pub fn parse(input: &[u8]) -> anyhow::Result> { - let mail = parse_mail(input)?; + let mail = MimePart::parse(input).with_context(|| { + format!( + "Report::parse top; input is {:?}", + String::from_utf8_lossy(input) + ) + })?; - if mail.ctype.mimetype != "multipart/report" { + if content_type(&mail).as_deref() != Some("multipart/report") { return Ok(None); } let mut original_message = None; - for part in &mail.subparts { - if part.ctype.mimetype == "message/rfc822" - || part.ctype.mimetype == "text/rfc822-headers" - { - let (_headers, offset) = parse_headers(part.raw_bytes)?; - original_message = - Some(String::from_utf8_lossy(&part.raw_bytes[offset..]).replace("\r\n", "\n")); + for part in mail.child_parts() { + let ct = content_type(part); + let ct = ct.as_deref(); + if ct == Some("message/rfc822") || ct == Some("text/rfc822-headers") { + original_message = Some(part.raw_body().replace("\r\n", "\n")); } } - for part in &mail.subparts { - if part.ctype.mimetype == "message/delivery-status" - || part.ctype.mimetype == "message/global-delivery-status" + for part in mail.child_parts() { + let ct = content_type(part); + let ct = ct.as_deref(); + if ct == Some("message/delivery-status") || ct == Some("message/global-delivery-status") { return Ok(Some(Self::parse_inner(part, original_message)?)); } @@ -257,8 +266,8 @@ impl Report { anyhow::bail!("delivery-status part missing"); } - fn parse_inner(part: &ParsedMail, original_message: Option) -> anyhow::Result { - let body = part.get_body()?; + fn parse_inner(part: &MimePart, original_message: Option) -> anyhow::Result { + let body = part.raw_body(); let body = body.replace("\r\n", "\n"); let mut parts = body.trim().split("\n\n"); diff --git a/crates/kumo-log-types/src/rfc5965.rs b/crates/kumo-log-types/src/rfc5965.rs index 51e1f390..2628c8e5 100644 --- a/crates/kumo-log-types/src/rfc5965.rs +++ b/crates/kumo-log-types/src/rfc5965.rs @@ -1,8 +1,8 @@ //! ARF reports -use crate::rfc3464::RemoteMta; +use crate::rfc3464::{content_type, RemoteMta}; use anyhow::anyhow; use chrono::{DateTime, Utc}; -use mailparse::{parse_headers, parse_mail, ParsedMail}; +use mailparsing::{Header, HeaderParseResult, MimePart}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::str::FromStr; @@ -43,34 +43,37 @@ pub struct ARFReport { impl ARFReport { pub fn parse(input: &[u8]) -> anyhow::Result> { - let mail = parse_mail(input)?; + let mail = MimePart::parse(input)?; + let ct = mail.headers().content_type()?; + let ct = match ct { + None => return Ok(None), + Some(ct) => ct, + }; - if mail.ctype.mimetype != "multipart/report" { + if ct.value != "multipart/report" { return Ok(None); } - if mail.ctype.params.get("report-type").map(|s| s.as_str()) != Some("feedback-report") { + + if ct.get("report-type").as_deref() != Some("feedback-report") { return Ok(None); } let mut original_message = None; let mut supplemental_trace = None; - for part in &mail.subparts { - if part.ctype.mimetype == "message/rfc822" - || part.ctype.mimetype == "text/rfc822-headers" - { - let (_headers, offset) = parse_headers(part.raw_bytes)?; - let bytes = &part.raw_bytes[offset..]; - - if let Ok((headers, _)) = parse_headers(bytes) { + for part in mail.child_parts() { + let ct = content_type(part); + let ct = ct.as_deref(); + if ct == Some("message/rfc822") || ct == Some("text/rfc822-headers") { + if let Ok(HeaderParseResult { headers, .. }) = + Header::parse_headers(part.raw_body()) + { // Look for x-headers that might be our supplemental trace headers - for hdr in headers { - if !(hdr.get_key_ref().starts_with("X-") - || hdr.get_key_ref().starts_with("x-")) - { + for hdr in headers.iter() { + if !(hdr.get_name().starts_with("X-") || hdr.get_name().starts_with("x-")) { continue; } - if let Ok(decoded) = base64::decode(hdr.get_value_raw()) { + if let Ok(decoded) = base64::decode(hdr.get_raw_value()) { #[derive(Deserialize)] struct Wrap { #[serde(rename = "_@_")] @@ -91,12 +94,14 @@ impl ARFReport { } } - original_message = Some(String::from_utf8_lossy(bytes).replace("\r\n", "\n")); + original_message = Some(part.raw_body().replace("\r\n", "\n")); } } - for part in &mail.subparts { - if part.ctype.mimetype == "message/feedback-report" { + for part in mail.child_parts() { + let ct = content_type(part); + let ct = ct.as_deref(); + if ct == Some("message/feedback-report") { return Ok(Some(Self::parse_inner( part, original_message, @@ -109,11 +114,11 @@ impl ARFReport { } fn parse_inner( - part: &ParsedMail, + part: &MimePart, original_message: Option, supplemental_trace: Option, ) -> anyhow::Result { - let body = part.get_body()?; + let body = part.raw_body(); let mut extensions = extract_headers(body.as_bytes())?; let feedback_type = extract_single_req("feedback-type", &mut extensions)?; @@ -153,16 +158,16 @@ impl ARFReport { } pub(crate) fn extract_headers(part: &[u8]) -> anyhow::Result>> { - let (headers, _) = parse_headers(part)?; + let HeaderParseResult { headers, .. } = Header::parse_headers(part)?; let mut extensions = HashMap::new(); - for hdr in headers { - let name = hdr.get_key_ref().to_ascii_lowercase(); + for hdr in headers.iter() { + let name = hdr.get_name().to_ascii_lowercase(); extensions .entry(name) .or_insert_with(|| vec![]) - .push(hdr.get_value_utf8()?); + .push(hdr.as_unstructured()?); } Ok(extensions) } diff --git a/crates/mailparsing/src/header.rs b/crates/mailparsing/src/header.rs index de13e692..9fce9ef6 100644 --- a/crates/mailparsing/src/header.rs +++ b/crates/mailparsing/src/header.rs @@ -202,13 +202,6 @@ impl<'a> Header<'a> { while idx < header_block.len() { let b = header_block[idx]; - if headers.is_empty() { - if b.is_ascii_whitespace() { - return Err(MailParsingError::HeaderParse( - "header block must not start with spaces".to_string(), - )); - } - } if b == b'\n' { // LF: End of header block idx += 1; @@ -225,6 +218,13 @@ impl<'a> Header<'a> { "lone CR in header".to_string(), )); } + if headers.is_empty() { + if b.is_ascii_whitespace() { + return Err(MailParsingError::HeaderParse( + "header block must not start with spaces".to_string(), + )); + } + } let (header, next) = Self::parse(header_block.slice(idx..header_block.len()))?; overall_conformance |= header.conformance; headers.push(header); diff --git a/crates/mailparsing/src/mimepart.rs b/crates/mailparsing/src/mimepart.rs index f775f680..f74916c5 100644 --- a/crates/mailparsing/src/mimepart.rs +++ b/crates/mailparsing/src/mimepart.rs @@ -131,12 +131,21 @@ impl<'a> MimePart<'a> { memchr::memchr(b'\n', &raw_body.as_bytes()[boundary_end..]) .map(|p| p + boundary_end + 1) { - let part_end = iter.next().unwrap_or(raw_body.len()); + let part_end = iter + .next() + .map(|p| { + // P is the newline; we want to include it in the raw + // bytes for this part, so look beyond it + p + 1 + }) + .unwrap_or(raw_body.len()); let child = Self::parse_impl(raw_body.slice(part_start..part_end), false)?; self.parts.push(child); - boundary_end = part_end + boundary.len(); + boundary_end = part_end - + 1 /* newline we adjusted for when assigning part_end */ + + boundary.len(); if boundary_end + 2 > raw_body.len() || &raw_body.as_bytes()[boundary_end..boundary_end + 2] == b"--" { @@ -352,7 +361,8 @@ Ok( r#" Ok( Text( - "This is the plaintext version, in utf-8. Proof by Euro: €", + "This is the plaintext version, in utf-8. Proof by Euro: €\r +", ), ) "#