diff --git a/crates/dkim/src/arc.rs b/crates/dkim/src/arc.rs new file mode 100644 index 00000000..ec895b7d --- /dev/null +++ b/crates/dkim/src/arc.rs @@ -0,0 +1,157 @@ +use crate::header::{ARCMessageSignatureHeader, ARCSealHeader}; +use crate::{verify_email_header, DKIMError, ParsedEmail}; +use dns_resolver::Resolver; +use mailparsing::ARCAuthenticationResults; +use std::collections::BTreeMap; + +pub const MAX_ARC_INSTANCE: u8 = 50; +pub const ARC_MESSAGE_SIGNATURE_HEADER_NAME: &str = "ARC-Message-Signature"; +pub const ARC_SEAL_HEADER_NAME: &str = "ARC-Seal"; +pub const ARC_AUTHENTICATION_RESULTS_HEADER_NAME: &str = "ARC-Authentication-Results"; + +#[derive(Debug)] +pub struct ARCSet { + pub aar: ARCAuthenticationResults, + pub seal: ARCSealHeader, + pub sig: ARCMessageSignatureHeader, +} + +impl ARCSet { + pub fn instance(&self) -> u8 { + self.aar.instance + } +} + +pub async fn analyze_arc(email: &ParsedEmail<'_>, resolver: &dyn Resolver) { + let mut seals = BTreeMap::new(); + let mut sigs = BTreeMap::new(); + let mut aars = BTreeMap::new(); + + let headers = email.get_headers(); + let mut issues = vec![]; + + for hdr in headers.iter_named(ARC_SEAL_HEADER_NAME) { + match ARCSealHeader::parse(hdr.get_raw_value()) { + Ok(seal) => { + let instance = seal.arc_instance().expect("validated by parse"); + seals.entry(instance).or_insert_with(Vec::new).push(seal); + } + Err(err) => { + issues.push(err); + } + } + } + + for hdr in headers.iter_named(ARC_MESSAGE_SIGNATURE_HEADER_NAME) { + match ARCMessageSignatureHeader::parse(hdr.get_raw_value()) { + Ok(sig) => { + let instance = sig.arc_instance().expect("validated by parse"); + sigs.entry(instance).or_insert_with(Vec::new).push(sig); + } + Err(err) => { + issues.push(err); + } + } + } + + for hdr in headers.iter_named(ARC_AUTHENTICATION_RESULTS_HEADER_NAME) { + match hdr.as_arc_authentication_results() { + Ok(aar) => { + if aar.instance == 0 || aar.instance > MAX_ARC_INSTANCE { + issues.push(DKIMError::InvalidARCInstance); + continue; + } + aars.entry(aar.instance).or_insert_with(Vec::new).push(aar); + } + Err(err) => { + issues.push(err.into()); + } + } + } + + let mut arc_sets = BTreeMap::new(); + for instance in 1..=MAX_ARC_INSTANCE { + match ( + seals.get(&instance), + sigs.get(&instance), + aars.get(&instance), + ) { + (Some(seal), Some(sig), Some(aar)) => { + if seal.len() > 1 || sig.len() > 1 || aar.len() > 1 { + issues.push(DKIMError::DuplicateARCInstance(instance)); + continue; + } + + arc_sets.insert( + instance, + ARCSet { + seal: seal[0].clone(), + sig: sig[0].clone(), + aar: aar[0].clone(), + }, + ); + } + (None, None, None) => { + // Not an error unless there are gaps; we'll check + // for that below + } + _ => { + // One or more are missing + issues.push(DKIMError::MissingARCInstance(instance)); + } + } + } + + // Ensure that the keys are contiguous + for instance in 2..=MAX_ARC_INSTANCE { + if arc_sets.contains_key(&instance) { + let prior = instance - 1; + if !arc_sets.contains_key(&prior) { + issues.push(DKIMError::MissingARCInstance(prior)); + } + } + } + + for arc_set in arc_sets.values() { + eprintln!("{arc_set:#?}"); + + eprintln!("Processing instance {}", arc_set.instance()); + if let Err(err) = verify_email_header( + resolver, + ARC_MESSAGE_SIGNATURE_HEADER_NAME, + &arc_set.sig, + email, + ) + .await + { + issues.push(err); + } + + // Verify the Seal + if let Err(err) = + verify_email_header(resolver, ARC_SEAL_HEADER_NAME, &arc_set.seal, email).await + { + issues.push(err); + } + } + + eprintln!("Issues: {issues:#?}"); +} + +#[cfg(test)] +mod test { + use super::*; + use dns_resolver::TestResolver; + + const EXAMPLE_MESSAGE: &str = include_str!("../test/arc-example.eml"); + + #[tokio::test] + async fn test_parse_example_from_rfc8617() { + let email = ParsedEmail::parse(EXAMPLE_MESSAGE.replace('\n', "\r\n")).unwrap(); + let resolver = TestResolver::default().with_txt( + "fm3._domainkey.messagingengine.com", + "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3TntGwdEtmIx+H8Etk1IgA2gLzy9v22TO+BcTUmUFaURWSG413g+VItm86ntW1bfbgFk/ArrTVAzQxgynoCQky3VXMXl2qEKgGSrLv+QaNvbebVDZI6VZX8D5+aJIN3sCSVY1eXA4x6LbPZ8pAqIAuAhtfXc7rVKbELqlEaUMrQ+ovyjF4R6gfL621BKdLeTF89/kbqJhLwmgtzok6UBUzexDDBhZ0gfGw331J+7aqdJLWUCQv6iE3zkI4myyEcMrgWxRjdZ861x374pNzady/B688A5i4BHoVnBJBuLEYfS1gTCC/7SB6U5AdEin3P0/+DqSH36cu8+MvAZ1C7E2wIDAQAB" + ); + analyze_arc(&email, &resolver).await; + } +} diff --git a/crates/dkim/src/errors.rs b/crates/dkim/src/errors.rs index 66365873..1e32f2ae 100644 --- a/crates/dkim/src/errors.rs +++ b/crates/dkim/src/errors.rs @@ -63,6 +63,12 @@ pub enum DKIMError { CanonicalLineEndingsRequired, #[error(transparent)] Dns(#[from] DnsError), + #[error("ARC instance tag is invalid")] + InvalidARCInstance, + #[error("ARC Set Instance {0} has duplicate headers")] + DuplicateARCInstance(u8), + #[error("ARC Set Instance {0} has missing headers")] + MissingARCInstance(u8), } impl DKIMError { @@ -85,6 +91,9 @@ impl DKIMError { | BodyHashDidNotVerify | MalformedBody | CanonicalLineEndingsRequired + | InvalidARCInstance + | DuplicateARCInstance(_) + | MissingARCInstance(_) | MailParsingError(_) | UnsupportedCanonicalizationType(_) | UnsupportedHashAlgorithm(_) => Status::Permfail, diff --git a/crates/dkim/src/hash.rs b/crates/dkim/src/hash.rs index 71358ce6..8d014c62 100644 --- a/crates/dkim/src/hash.rs +++ b/crates/dkim/src/hash.rs @@ -1,6 +1,6 @@ -use crate::header::DKIM_SIGNATURE_HEADER_NAME; -use crate::{canonicalization, DKIMError, DKIMHeader, ParsedEmail}; +use crate::{canonicalization, DKIMError, ParsedEmail, TaggedHeader}; use data_encoding::BASE64; +use mailparsing::Header; use sha1::{Digest as _, Sha1}; use sha2::Sha256; use std::collections::HashMap; @@ -118,33 +118,23 @@ pub(crate) fn compute_body_hash<'a>( /// Holds a list of header names, normalized to lower case #[derive(Debug)] -pub(crate) enum HeaderList { - /// A list of possibly duplicated header names - MaybeMultiple(Vec), - /// A list of of unique header names - Unique(Vec), -} +pub(crate) struct HeaderList(Vec); impl HeaderList { pub fn as_h_list(&self) -> String { - match self { - Self::MaybeMultiple(list) | Self::Unique(list) => list.join(":"), - } + self.0.join(":") } /// Computes the header list that should be used to over-sign /// the provided email message, and returns it pub fn compute_over_signed(&self, email: &ParsedEmail) -> Self { - let unique_header_names = match self { - Self::Unique(names) => names.clone(), - Self::MaybeMultiple(names) => { - // Note that Self::new() normalized the names, so we can - // simply dedup without further concern for normalization - let mut n = names.clone(); - n.sort(); - n.dedup(); - n - } + let unique_header_names = { + // Note that Self::new() normalized the names, so we can + // simply dedup without further concern for normalization + let mut n = self.0.clone(); + n.sort(); + n.dedup(); + n }; let email_headers = email.get_headers(); @@ -157,63 +147,17 @@ impl HeaderList { result.push(name); } - Self::MaybeMultiple(result) + Self(result) } /// Build a header list. /// Analyzes the list to determine whether it is a unique list or not pub fn new(list: Vec) -> Self { let normalized: Vec = list.into_iter().map(|s| s.to_ascii_lowercase()).collect(); - - let mut all_single = true; - for name in &normalized { - let n: usize = normalized - .iter() - .map(|candidate| if candidate == name { 1 } else { 0 }) - .sum(); - if n > 1 { - all_single = false; - break; - } - } - - if all_single { - Self::Unique(normalized) - } else { - Self::MaybeMultiple(normalized) - } + Self(normalized) } - /// Apply `apply` to each header in the provided email that - /// matches the headers, follow the order set out in Section 5.4.2 - fn apply<'a, F: FnMut(&'a str, &'a [u8])>(&self, email: &'a ParsedEmail, apply: F) { - match self { - Self::MaybeMultiple(list) => Self::apply_multiple(list, email, apply), - Self::Unique(list) => Self::apply_unique(list, email, apply), - } - } - - /// Perform the apply when we know that the list of header names - /// are unique. - /// We can avoid allocating any additional state for this case. - fn apply_unique<'a, F: FnMut(&'a str, &'a [u8])>( - header_list: &[String], - email: &'a ParsedEmail, - mut apply: F, - ) { - let email_headers = email.get_headers(); - - 'outer: for name in header_list { - for header in email_headers.iter().rev() { - if header.get_name().eq_ignore_ascii_case(name) { - apply(header.get_name(), header.get_raw_value().as_bytes()); - continue 'outer; - } - } - } - } - - /// Section 5.4.2: + /// matches the headers, returning them in the order set out in Section 5.4.2 /// Signers wishing to sign multiple instances of such a header field MUST /// include the header field name multiple times in the "h=" tag of the /// DKIM-Signature header field and MUST sign such header fields in order @@ -222,11 +166,8 @@ impl HeaderList { /// To facilitate this, we need to maintain state for each header name /// in the list to ensure that we select the appropriate header in the /// appropriate order. - fn apply_multiple<'a, F: FnMut(&'a str, &'a [u8])>( - header_list: &[String], - email: &'a ParsedEmail, - mut apply: F, - ) { + fn compute_concrete_header_list<'a>(&self, email: &'a ParsedEmail) -> Vec<&'a Header<'a>> { + let mut headers = vec![]; let email_headers = email.get_headers(); let num_headers = email_headers.len(); @@ -234,7 +175,7 @@ impl HeaderList { // to lower case. That happens in our constructor. let mut last_index: HashMap<&String, usize> = HashMap::new(); - 'outer: for name in header_list { + 'outer: for name in &self.0 { let index = last_index.get(name).unwrap_or(&num_headers); for (header_index, header) in email_headers .iter() @@ -243,7 +184,7 @@ impl HeaderList { .skip(num_headers - index) { if header.get_name().eq_ignore_ascii_case(name) { - apply(header.get_name(), header.get_raw_value().as_bytes()); + headers.push(header); last_index.insert(name, header_index); continue 'outer; } @@ -253,9 +194,11 @@ impl HeaderList { // treated as the null string (including the header field name, header // field value, all punctuation, and the trailing CRLF). // -> don't include it in the returned signed_headers. + // FIXME: something is fishy here. last_index.insert(name, 0); } + headers } } @@ -263,15 +206,21 @@ pub(crate) fn compute_headers_hash<'a>( canonicalization_type: canonicalization::Type, headers: &HeaderList, hash_algo: HashAlgo, - dkim_header: &DKIMHeader, + dkim_header: &TaggedHeader, email: &'a ParsedEmail<'a>, + signature_header_name: &str, ) -> Result, DKIMError> { let mut input = Vec::new(); let mut hasher = HashImpl::from_algo(hash_algo); - headers.apply(email, |key, value| { - canonicalization_type.canon_header_into(key, value, &mut input); - }); + let concrete = headers.compute_concrete_header_list(email); + for header in concrete { + canonicalization_type.canon_header_into( + header.get_name(), + header.get_raw_value().as_bytes(), + &mut input, + ); + } // Add the DKIM-Signature header in the hash. Remove the value of the // signature (b) first. @@ -280,7 +229,7 @@ pub(crate) fn compute_headers_hash<'a>( let value = dkim_header.raw().replace(sign, ""); let mut canonicalized_value = vec![]; canonicalization_type.canon_header_into( - DKIM_SIGNATURE_HEADER_NAME, + signature_header_name, value.as_bytes(), &mut canonicalized_value, ); @@ -300,9 +249,10 @@ pub(crate) fn compute_headers_hash<'a>( #[cfg(test)] mod tests { use super::*; + use crate::{DKIMHeader, DKIM_SIGNATURE_HEADER_NAME}; fn dkim_header() -> DKIMHeader { - crate::DKIMHeader::parse("v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=smtp; d=test.com; t=1641506955; h=content-type:to: subject:date:from:mime-version:sender; bh=PU2XIErWsXvhvt1W96ntPWZ2VImjVZ3vBY2T/A+wA3A=; b=PIO0A014nyntOGKdTdtvCJor9ZxvP1M3hoLeEh8HqZ+RvAyEKdAc7VOg+/g/OTaZgsmw6U sZCoN0YNVp+2o9nkaeUslsVz3M4I55HcZnarxl+fhplIMcJ/3s0nIhXL51MfGPRqPbB7/M Gjg9/07/2vFoid6Kitg6Z+CfoD2wlSRa8xDfmeyA2cHpeVuGQhGxu7BXuU8kGbeM4+weit Ql3t9zalhikEPI5Pr7dzYFrgWNOEO6w6rQfG7niKON1BimjdbJlGanC7cO4UL361hhXT4X iXLnC9TG39xKFPT/+4nkHy8pp6YvWkD3wKlBjwkYNm0JvKGwTskCMDeTwxXhAg==").unwrap() + DKIMHeader::parse("v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=smtp; d=test.com; t=1641506955; h=content-type:to: subject:date:from:mime-version:sender; bh=PU2XIErWsXvhvt1W96ntPWZ2VImjVZ3vBY2T/A+wA3A=; b=PIO0A014nyntOGKdTdtvCJor9ZxvP1M3hoLeEh8HqZ+RvAyEKdAc7VOg+/g/OTaZgsmw6U sZCoN0YNVp+2o9nkaeUslsVz3M4I55HcZnarxl+fhplIMcJ/3s0nIhXL51MfGPRqPbB7/M Gjg9/07/2vFoid6Kitg6Z+CfoD2wlSRa8xDfmeyA2cHpeVuGQhGxu7BXuU8kGbeM4+weit Ql3t9zalhikEPI5Pr7dzYFrgWNOEO6w6rQfG7niKON1BimjdbJlGanC7cO4UL361hhXT4X iXLnC9TG39xKFPT/+4nkHy8pp6YvWkD3wKlBjwkYNm0JvKGwTskCMDeTwxXhAg==").unwrap() } #[test] @@ -436,7 +386,8 @@ Hello Alice &headers, hash_algo, &dkim_header(), - &email + &email, + DKIM_SIGNATURE_HEADER_NAME ) .unwrap(), &[ @@ -451,7 +402,8 @@ Hello Alice &headers, hash_algo, &dkim_header(), - &email + &email, + DKIM_SIGNATURE_HEADER_NAME ) .unwrap(), &[ @@ -481,7 +433,8 @@ Hello Alice &headers, hash_algo, &dkim_header(), - &email + &email, + DKIM_SIGNATURE_HEADER_NAME ) .unwrap(), &[ @@ -496,7 +449,8 @@ Hello Alice &headers, hash_algo, &dkim_header(), - &email + &email, + DKIM_SIGNATURE_HEADER_NAME ) .unwrap(), &[ @@ -516,11 +470,11 @@ Hello Alice header_list: &HeaderList, email: &'a ParsedEmail, ) -> Vec<(&'a str, &'a [u8])> { - let mut result = vec![]; - header_list.apply(email, |key, value| { - result.push((key, value)); - }); - result + header_list + .compute_concrete_header_list(email) + .into_iter() + .map(|header| (header.get_name(), header.get_raw_value().as_bytes())) + .collect() } #[test] diff --git a/crates/dkim/src/header.rs b/crates/dkim/src/header.rs index f4bf8fb5..70c6db32 100644 --- a/crates/dkim/src/header.rs +++ b/crates/dkim/src/header.rs @@ -1,3 +1,4 @@ +use crate::arc::MAX_ARC_INSTANCE; use crate::{parser, DKIMError, HeaderList}; use dns_resolver::Name; use indexmap::map::IndexMap; @@ -78,6 +79,19 @@ impl TaggedHeader { &self.raw_bytes } + pub fn arc_instance(&self) -> Result { + let instance = self + .get_required_tag("i") + .parse::() + .map_err(|_| DKIMError::InvalidARCInstance)?; + + if instance == 0 || instance > MAX_ARC_INSTANCE { + return Err(DKIMError::InvalidARCInstance); + } + + Ok(instance) + } + /// Generate the DKIM-Signature header from the tags fn serialize(&self) -> String { let mut out = String::new(); @@ -311,7 +325,7 @@ impl DKIMHeaderBuilder { /// DKIM; instead, this tag is replaced by the instance tag as defined /// in Section 4.2.1. #[derive(Debug, Clone, Default)] -pub(crate) struct ARCMessageSignatureHeader { +pub struct ARCMessageSignatureHeader { tagged: TaggedHeader, } @@ -334,12 +348,13 @@ impl ARCMessageSignatureHeader { header.validate_required_tags()?; header.check_common_tags()?; + header.arc_instance()?; Ok(header) } fn validate_required_tags(&self) -> Result<(), DKIMError> { - const REQUIRED_TAGS: &[&str] = &["a", "b", "bh", "d", "h", "s"]; + const REQUIRED_TAGS: &[&str] = &["a", "b", "bh", "d", "h", "s", "i"]; for required in REQUIRED_TAGS { if self.get_tag(required).is_none() { return Err(DKIMError::SignatureMissingRequiredTag(required)); @@ -350,7 +365,7 @@ impl ARCMessageSignatureHeader { } #[derive(Debug, Clone, Default)] -pub(crate) struct ARCSealHeader { +pub struct ARCSealHeader { tagged: TaggedHeader, } @@ -369,15 +384,39 @@ impl std::ops::DerefMut for ARCSealHeader { impl ARCSealHeader { pub fn parse(value: &str) -> Result { let tagged = TaggedHeader::parse(value)?; - let header = Self { tagged }; + let mut header = Self { tagged }; header.validate_required_tags()?; + header.arc_instance()?; + + if header.get_tag("h").is_some() { + // TODO: MUST result in cv status of fail, see Section 5.1.1 + } + + // Force in some values so that we can re-use the rest of + // the dkim infra on this header type + header.set_tag( + "h", + "ARC-Authentication-Results:ARC-Message-Signature:ARC-Seal", + ); + header.set_tag("c", "relaxed"); Ok(header) } + fn set_tag(&mut self, name: &str, value: &str) { + self.tagged.tags.insert( + name.to_string(), + parser::Tag { + name: name.to_string(), + value: value.to_string(), + raw_value: value.to_string(), + }, + ); + } + fn validate_required_tags(&self) -> Result<(), DKIMError> { - const REQUIRED_TAGS: &[&str] = &["a", "b", "d", "s"]; + const REQUIRED_TAGS: &[&str] = &["a", "b", "d", "s", "i"]; for required in REQUIRED_TAGS { if self.get_tag(required).is_none() { return Err(DKIMError::SignatureMissingRequiredTag(required)); diff --git a/crates/dkim/src/lib.rs b/crates/dkim/src/lib.rs index eedfa27f..e5e58749 100644 --- a/crates/dkim/src/lib.rs +++ b/crates/dkim/src/lib.rs @@ -2,6 +2,7 @@ use crate::errors::Status; use crate::hash::HeaderList; +use crate::header::TaggedHeader; use dns_resolver::{HickoryResolver, Resolver}; use ed25519_dalek::pkcs8::DecodePrivateKey; use ed25519_dalek::SigningKey; @@ -12,6 +13,7 @@ use openssl::pkey_ctx::PkeyCtx; use openssl::rsa::{Padding, Rsa}; use std::collections::BTreeMap; +pub mod arc; pub mod canonicalization; mod errors; mod hash; @@ -175,7 +177,8 @@ fn verify_signature( async fn verify_email_header<'a>( resolver: &dyn Resolver, - dkim_header: &'a DKIMHeader, + signature_header_name: &str, + dkim_header: &'a TaggedHeader, email: &'a ParsedEmail<'a>, ) -> Result<(), DKIMError> { let public_keys = public_key::retrieve_public_keys( @@ -188,12 +191,6 @@ async fn verify_email_header<'a>( let (header_canonicalization_type, body_canonicalization_type) = parser::parse_canonicalization(dkim_header.get_tag("c"))?; let hash_algo = parser::parse_hash_algo(dkim_header.get_required_tag("a"))?; - let computed_body_hash = hash::compute_body_hash( - body_canonicalization_type, - dkim_header.parse_tag("l")?, - hash_algo, - email, - )?; let header_list: Vec = dkim_header .get_required_tag("h") @@ -207,12 +204,20 @@ async fn verify_email_header<'a>( hash_algo, dkim_header, email, + signature_header_name, )?; - tracing::debug!("body_hash {:?}", computed_body_hash); - let header_body_hash = dkim_header.get_required_tag("bh"); - if header_body_hash != computed_body_hash { - return Err(DKIMError::BodyHashDidNotVerify); + if let Some(header_body_hash) = dkim_header.get_tag("bh") { + let computed_body_hash = hash::compute_body_hash( + body_canonicalization_type, + dkim_header.parse_tag("l")?, + hash_algo, + email, + )?; + tracing::debug!("body_hash {:?}", computed_body_hash); + if header_body_hash != computed_body_hash { + return Err(DKIMError::BodyHashDidNotVerify); + } } let signature = data_encoding::BASE64 @@ -320,25 +325,28 @@ pub async fn verify_email_with_resolver<'a>( props.insert("header.b".to_string(), b_tag); let mut reason = None; - let result = match verify_email_header(resolver, dkim_header, email).await { - Ok(()) => { - if signing_domain.eq_ignore_ascii_case(from_domain) { - "pass" - } else { - let why = "mail-from-mismatch-signing-domain".to_string(); - reason.replace(why.clone()); - props.insert("policy.dkim-rules".to_string(), why); - "policy" + let result = + match verify_email_header(resolver, DKIM_SIGNATURE_HEADER_NAME, dkim_header, email) + .await + { + Ok(()) => { + if signing_domain.eq_ignore_ascii_case(from_domain) { + "pass" + } else { + let why = "mail-from-mismatch-signing-domain".to_string(); + reason.replace(why.clone()); + props.insert("policy.dkim-rules".to_string(), why); + "policy" + } } - } - Err(err) => { - reason.replace(format!("{err}")); - match err.status() { - Status::Tempfail => "temperror", - Status::Permfail => "permerror", + Err(err) => { + reason.replace(format!("{err}")); + match err.status() { + Status::Tempfail => "temperror", + Status::Permfail => "permerror", + } } - } - }; + }; results.push(AuthenticationResult { method: "dkim".to_string(), @@ -506,6 +514,7 @@ $ORIGIN brisbane._domainkey.football.example.com verify_email_header( &resolver, + DKIM_SIGNATURE_HEADER_NAME, &DKIMHeader::parse(raw_header_dkim).unwrap(), &email, ) @@ -555,6 +564,7 @@ Joe. verify_email_header( &resolver, + DKIM_SIGNATURE_HEADER_NAME, &DKIMHeader::parse(raw_header_rsa).unwrap(), &email, ) diff --git a/crates/dkim/src/sign.rs b/crates/dkim/src/sign.rs index e24bc1bd..1feb169e 100644 --- a/crates/dkim/src/sign.rs +++ b/crates/dkim/src/sign.rs @@ -295,6 +295,7 @@ impl Signer { self.hash_algo, &dkim_header, email, + DKIM_SIGNATURE_HEADER_NAME, ) } } diff --git a/crates/dkim/test/arc-example-2.eml b/crates/dkim/test/arc-example-2.eml new file mode 100644 index 00000000..d9385fcb --- /dev/null +++ b/crates/dkim/test/arc-example-2.eml @@ -0,0 +1,185 @@ +Return-Path: +Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) + by slotpi15m71 (Cyrus 3.13.0-alpha0-1341-g6c10bd1c5-fm-20251013.001-g6c10bd1c) with LMTPA; + Thu, 30 Oct 2025 05:58:08 -0400 +X-Cyrus-Session-Id: slotpi15m71-1761818288-1181264-2-10905896220539992579 +X-Sieve: CMU Sieve 3.0 +X-Spam-known-sender: yes ("Self sent message"); in-addressbook, self-send; + 2A9FBB1A-0A50-11F0-953D-C6A695ACF6E2 ("Imported 20250326") +X-Spam-sender-reputation: 999 (email; noauth) +X-Spam-score: 0.0 +X-Spam-hits: ALL_TRUSTED -1, BAYES_00 -1.9, ME_FROM_EQ_TO 0.01, ME_SC_SENDERREP -100, + ME_SENDERREP_ALLOW -4, SHORTCIRCUIT -0.0001, SPF_PASS -0.001, + LANGUAGES unknown, BAYES_USED user, SA_VERSION 4.0.1 +X-Spam-source: IP='Unknown', Host='unk', Country='unk', FromHeader='org', MailFrom='org' +X-Spam-charsets: +X-Resolved-to: wez@wezfurlong.org +X-Delivered-to: wez@wezfurlong.org +X-Mail-from: wez@wezfurlong.org +Received: from phl-mx-07 ([10.202.2.206]) + by phl-compute-02.internal (LMTPProxy); Thu, 30 Oct 2025 05:58:08 -0400 +Received: from phl-mx-07.messagingengine.com (localhost [127.0.0.1]) + by mailmx.phl.internal (Postfix) with ESMTP id F38B720E00E0 + for ; Thu, 30 Oct 2025 05:58:07 -0400 (EDT) +Received: from mailmx.phl.internal (localhost [127.0.0.1]) + by phl-mx-07.messagingengine.com (Authentication Milter) with ESMTP + id FA4D1679830.F2D1A20E00B4; + Thu, 30 Oct 2025 05:58:07 -0400 +ARC-Seal: i=1; a=rsa-sha256; cv=none; d=messagingengine.com; s=fm3; t= + 1761818287; b=ycwa8bnZ1tzWb6H7JnfvMbAcXQarSXzeH4FF70n897IuqJjpK5 + m0OEH28pRIMClrC7MyimD9LcccWGTipSLMnEpv0KmI9vR2U6XhP4H4GRjTPllIkA + iUmAlS3sBmZTgfYSA/G1EbNaT4Nze7gnls5nRJkzxr1fj8uISFwMxkbNLam4KX/7 + +LzoKHUjI73wSb163y3j6iPb4haU7pxtuBoStGBlh0iZmnM+uj1akTbJnKr7VHKS + Ei84Pwc6wiRDjGwR7L8WUNJ1jOUaxHs+9fBDf03DL/yRmLIuVo14l/vjN/GV/9Ho + ZYblFYalGQ/0vYo06nbN1AprpO02h75DX8TA== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=mime-version:date:from:to:message-id + :subject:content-type:content-transfer-encoding; s=fm3; t= + 1761818287; bh=CuO/UH3bRHVPCZuIP5Cvk96qUDS4wL+5Fe9pIlYTsgg=; b=W + a3kfSabLIPfuoIoSAfny5UlDGvvoBePo0x/2cZgy/GMN8FVtYZVJZtZDvzuvk25B + O5zNW4x3ygfJ+jH0Y5F4GGkHXV0G2qBL+Qmr+CRZHL3dbEQOD+y55W+E0gdXxrhw + anst7erKfRpLu+aJNn6T10YTKKBwRmipOAM4IuyQQI2E8W3cp15jfInM/v+EkiGm + NV03GyAWGX+UPhs0BVqvLEN7a+PobwQQiDVLEY7NF4y5E+c810RphyVXHvPqlIF2 + oTv7SCdoBEIA5Mw0F+HuY3jsK43xXbdolaoXb/u5Qdr8F/rwu3jEOlDP0awfg94e + COR4JQJdVrWjpQFxrG4Rw== +ARC-Authentication-Results: i=1; phl-mx-07.messagingengine.com; + x-csa=none; + x-me-sender=pass + policy.xms= + rzYDaZmgN376x5oNMHG94qT3s0y89wRpz7PaOSKeSVnszK_f19i0FzFocbt-07jXzmTQDndlWgz5yZX37AqB44eZE_Zhsr3TWTzd5ssnPy6whnBK6AvQPK-XDevRVZ_bz8E6Fk3RSukcnrmnOPMVHcqttFAAw27cCKjC3tNcZ2w; + x-ptr=pass smtp.helo=fout-a1-smtp.messagingengine.com + policy.ptr=fout-a1-smtp.messagingengine.com; + bimi=skipped (DMARC did not pass); + arc=none (no signatures found); + dkim=pass (2048-bit rsa key sha256) header.d=messagingengine.com + header.i=@messagingengine.com header.b=LeEj0x/z header.a=rsa-sha256 + header.s=fm3; + dkim=pass (2048-bit rsa key sha256) header.d=wezfurlong.org + header.i=@wezfurlong.org header.b=ij2uTb0J header.a=rsa-sha256 + header.s=fm1; + dmarc=none policy.published-domain-policy=none + policy.applied-disposition=none policy.evaluated-disposition=none + (p=none,d=none,d.eval=none) policy.policy-from=p + header.from=wezfurlong.org; + iprev=pass smtp.remote-ip=103.168.172.144 + (fout-a1-smtp.messagingengine.com); + spf=pass smtp.mailfrom=wez@wezfurlong.org + smtp.helo=fout-a1-smtp.messagingengine.com +X-ME-Authentication-Results: phl-mx-07.messagingengine.com; + x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 + smtp.bits=256/256; + x-vs=clean score=15 state=0 +Authentication-Results: phl-mx-07.messagingengine.com; + x-csa=none; + x-me-sender=pass + policy.xms= + rzYDaZmgN376x5oNMHG94qT3s0y89wRpz7PaOSKeSVnszK_f19i0FzFocbt-07jXzmTQDndlWgz5yZX37AqB44eZE_Zhsr3TWTzd5ssnPy6whnBK6AvQPK-XDevRVZ_bz8E6Fk3RSukcnrmnOPMVHcqttFAAw27cCKjC3tNcZ2w; + x-ptr=pass smtp.helo=fout-a1-smtp.messagingengine.com + policy.ptr=fout-a1-smtp.messagingengine.com +Authentication-Results: phl-mx-07.messagingengine.com; + bimi=skipped (DMARC did not pass) +Authentication-Results: phl-mx-07.messagingengine.com; + arc=none (no signatures found) +Authentication-Results: phl-mx-07.messagingengine.com; + dkim=pass (2048-bit rsa key sha256) header.d=messagingengine.com + header.i=@messagingengine.com header.b=LeEj0x/z header.a=rsa-sha256 + header.s=fm3; + dkim=pass (2048-bit rsa key sha256) header.d=wezfurlong.org + header.i=@wezfurlong.org header.b=ij2uTb0J header.a=rsa-sha256 + header.s=fm1; + dmarc=none policy.published-domain-policy=none + policy.applied-disposition=none policy.evaluated-disposition=none + (p=none,d=none,d.eval=none) policy.policy-from=p + header.from=wezfurlong.org; + iprev=pass smtp.remote-ip=103.168.172.144 + (fout-a1-smtp.messagingengine.com); + spf=pass smtp.mailfrom=wez@wezfurlong.org + smtp.helo=fout-a1-smtp.messagingengine.com +X-ME-VSCause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggdduieeifeduucetufdoteggodetrf + dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu + rghilhhouhhtmecufedttdenucgoteeftdduqddtudculdduhedmnecujfgurhepofggff + fhvffkufgtgfesthejredtredttdenucfhrhhomhepfdghvgiiucfhuhhrlhhonhhgfdcu + oeifvgiiseifvgiifhhurhhlohhnghdrohhrgheqnecuggftrfgrthhtvghrnhepgfdttd + efjedvffdthedtgeeufedtgeevkeevvedufeeivedttefhvdeludeltdeinecukfhppedu + tdefrdduieekrddujedvrddugeegnecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrg + hmpehinhgvthepuddtfedrudeikedrudejvddrudeggedphhgvlhhopehfohhuthdqrgdu + qdhsmhhtphdrmhgvshhsrghgihhnghgvnhhgihhnvgdrtghomhdpmhgrihhlfhhrohhmpe + eofigviiesfigviihfuhhrlhhonhhgrdhorhhgqedpnhgspghrtghpthhtohepuddprhgt + phhtthhopeeofigviiesfigviihfuhhrlhhonhhgrdhorhhgqe +X-ME-VSScore: 15 +X-ME-VSCategory: clean +X-ME-CSA: none +X-ME-Received: +X-ME-Received: +Received-SPF: pass + (wezfurlong.org: Sender is authorized to use 'wez@wezfurlong.org' in 'mfrom' identity (mechanism 'include:spf.messagingengine.com' matched)) + receiver=phl-mx-07.messagingengine.com; + identity=mailfrom; + envelope-from="wez@wezfurlong.org"; + helo=fout-a1-smtp.messagingengine.com; + client-ip=103.168.172.144 +Received: from mailfout.phl.internal (phl-outbound-01.internal [10.202.2.181]) + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) + key-exchange X25519 server-signature RSA-PSS (2048 bits)) + (No client certificate requested) + by mailmx.phl.internal (Postfix) with ESMTPS id F2D1A20E00B4 + for ; Thu, 30 Oct 2025 05:58:06 -0400 (EDT) +Received: from phl-compute-01.internal (phl-compute-01.internal [10.202.2.41]) + by mailfout.phl.internal (Postfix) with ESMTP id E5C3FEC01A3 + for ; Thu, 30 Oct 2025 05:58:06 -0400 (EDT) +Received: from phl-imap-15 ([10.202.2.104]) + by phl-compute-01.internal (MEProxy); Thu, 30 Oct 2025 05:58:06 -0400 +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=wezfurlong.org; + h=cc:content-transfer-encoding:content-type:content-type:date + :date:from:from:in-reply-to:message-id:mime-version:reply-to + :subject:subject:to:to; s=fm1; t=1761818286; x=1761904686; bh=Cu + O/UH3bRHVPCZuIP5Cvk96qUDS4wL+5Fe9pIlYTsgg=; b=ij2uTb0JGMxJvGQoT0 + 9FxAxNWQ113YYjX1IlhJd8oXCQVLthnji9nzjg8PQ1T39xMqp0qh0J7NTfOFl0si + yPWrrFDU3LBtMWll5XwmoC6ghavGy4rxx8uVK/MZT6zqwYzMqmhXWER0JlVDFzf9 + 7SkYJSwJR/sINOP776xZWTPxICxHISPd+pq5mqdKXY/4KemSXwbdF6ugIjI6rHwM + vYBF/wwiMjTTjP7uuvQTJIX+UgOUBEukKuQhOcxNo0wmWKP0xm78G8C1sq+gX6nf + 6y7ildrW2AY1QUgNv5zQXho2RLT1SlkoPfThbfDw/ujuDDQaafn7kxgVID/P6trt + 1+BQ== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=cc:content-transfer-encoding:content-type + :content-type:date:date:feedback-id:feedback-id:from:from + :in-reply-to:message-id:mime-version:reply-to:subject:subject:to + :to:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t= + 1761818286; x=1761904686; bh=CuO/UH3bRHVPCZuIP5Cvk96qUDS4wL+5Fe9 + pIlYTsgg=; b=LeEj0x/z/0dOVL2vxLMe45R5kq47+dt/yOGqeZvdMaritIeKOcG + DMNHzjiEEWGKvb19AdGuJ/ybq1DgF85W6bivYlr95/BJAz7+BBHrpfC6HYR3b/hs + Xu1YT0ksPJ0V+EGUcHrNTIMXXyHTpe2rw4FcKLxF2NX06+T3l7BDVMwitHiluQke + vc5ipeFoAL0fQrncLFMeX2eDLQ0L6W9gaxy5TKrcg4JHSGQyZm/5Y545CXKktdrg + kE1ky1c+Fat9xwJX3NgEZEmAMG7+tUPxVBWg9nVA95lyvQILDs31r6Q+vZyXopXb + 2JOF5y5FGkYshq3N+nbSifQwbMMBwRb9uXw== +X-ME-Sender: + +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggdduieeifeduucetufdoteggodetrf + dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu + rghilhhouhhtmecufedttdenucgoteeftdduqddtudculdduhedmnecujfgurhepofggff + fhvffkufgtgfesthejredtredttdenucfhrhhomhepfdghvgiiucfhuhhrlhhonhhgfdcu + oeifvgiiseifvgiifhhurhhlohhnghdrohhrgheqnecuggftrfgrthhtvghrnhepgfdttd + efjedvffdthedtgeeufedtgeevkeevvedufeeivedttefhvdeludeltdeinecuvehluhhs + thgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepfigviiesfigviihfuh + hrlhhonhhgrdhorhhgpdhnsggprhgtphhtthhopedupdhmohguvgepshhmthhpohhuthdp + rhgtphhtthhopeifvgiiseifvgiifhhurhhlohhnghdrohhrgh +X-ME-Proxy: + + + + +Feedback-ID: i26884094:Fastmail +Received: by mailuser.phl.internal (Postfix, from userid 501) + id 8D1B3780076; Thu, 30 Oct 2025 05:58:06 -0400 (EDT) +X-Mailer: MessagingEngine.com Webmail Interface +MIME-Version: 1.0 +Date: Thu, 30 Oct 2025 09:57:45 +0000 +From: "Wez Furlong" +To: "Wez Furlong" +Message-Id: <296b6703-4af3-4d92-b5ad-dde20b9ff61d@app.fastmail.com> +Subject: testing arc +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +another test for arc diff --git a/crates/dkim/test/arc-example.eml b/crates/dkim/test/arc-example.eml new file mode 100644 index 00000000..fcc367ce --- /dev/null +++ b/crates/dkim/test/arc-example.eml @@ -0,0 +1,227 @@ +Return-Path: +Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) + by slotpi15m71 (Cyrus 3.13.0-alpha0-1341-g6c10bd1c5-fm-20251013.001-g6c10bd1c) with LMTPA; + Thu, 30 Oct 2025 05:42:59 -0400 +X-Cyrus-Session-Id: slotpi15m71-1761817379-4093319-2-8826037471221011994 +X-Sieve: CMU Sieve 3.0 +X-Spam-known-sender: yes ("Address wez@kumomta.com in From header is in addressbook"); + in-addressbook; 42641a0a-c8ba-4e19-870f-1584b9a291f5 ("Autosaved") +X-Spam-sender-reputation: 1000 (email) +X-Spam-score: 0.0 +X-Spam-hits: BAYES_00 -1.9, ME_SC_SENDERREP -100, ME_SENDERREP_ALLOW -4, + RCVD_IN_DNSWL_NONE -0.0001, RCVD_IN_MSPIKE_H2 0.001, + SHORTCIRCUIT -0.0001, SPF_HELO_NONE 0.001, SPF_PASS -0.001, + LANGUAGES unknown, BAYES_USED user, SA_VERSION 4.0.1 +X-Spam-source: IP='209.85.222.174', Host='mail-qk1-f174.google.com', Country='US', + FromHeader='com', MailFrom='com' +X-Spam-charsets: +X-Resolved-to: wez@wezfurlong.org +X-Delivered-to: wez@wezfurlong.org +X-Mail-from: wez@kumomta.com +Received: from phl-mx-04 ([10.202.2.203]) + by phl-compute-02.internal (LMTPProxy); Thu, 30 Oct 2025 05:42:59 -0400 +Received: from phl-mx-04.messagingengine.com (localhost [127.0.0.1]) + by mailmx.phl.internal (Postfix) with ESMTP id 740A61380093 + for ; Thu, 30 Oct 2025 05:42:58 -0400 (EDT) +Received: from mailmx.phl.internal (localhost [127.0.0.1]) + by phl-mx-04.messagingengine.com (Authentication Milter) with ESMTP + id 45C04F27DE4.191D31380068; + Thu, 30 Oct 2025 05:42:58 -0400 +ARC-Seal: i=1; a=rsa-sha256; cv=none; d=messagingengine.com; s=fm3; t= + 1761817378; b=xiPk/Etps3qMOwoWhXiJyxGxMtV3XC0U6OtYQX1ORnaq8Bj7jH + /OgegZSo4PH1FXl4kuWfqYvVszPrSXLAtxybz2usLE37d2NO531QeoWEHGQK39bI + acPCAJV3NzD5GbbtaHI7eCzO0H1GKhEn07yfGbo/VTWalVNXHlXov2mySzEJl/lE + Q6do2hF7DbBif5ChICfGmWu/xS90t13D9EYg+eMPmg7QL719nJ66u7txtMCzXQ01 + ReDn9Xk+/kqwXPFh/6MWvJztPs3j8goi3qHo5gIh7Tj/S7yhGNpDz1lJwlJFrJXW + lv+TEnKLfEZ5r+bo9V+DO1RIrHNvrLSs6YFA== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d= + messagingengine.com; h=mime-version:date:from:to:message-id + :subject:content-type:content-transfer-encoding; s=fm3; t= + 1761817378; bh=B4Sdwm/LsvO9X1e98hS643RXXxvU5oFkgjJHmUF8s3k=; b=X + 7ABn3lCWF9ZaYd1xPhmzqJFHGCKOP2OJUbmH7osxevxAdHiSkSyeU3NS+mYbYNXZ + t8UlZfhdWlX+EBQcbr85qAnYwACjqaCBaX2kIJa/6SRWVS17f1ZLuUX5gWH4lv9l + nfoigfvAJWG50gQ51BFKiIthzLg/vWRpgGHh/RyMmmVnQIrPLkar/0rj4f1HQaaO + pTUhrAY9uVzXmQT4m/oGt/m05k+GPTGmQq6AHs3QHxmNRDGcGfdgmP3laHwIN4Y7 + XJ19+f8ZGmfKRCr0scORyXO9iDYbFjnfu7AUnNrBXXZRO1vOKXgV/jL0cdP4Ux2l + wabpV6OSsLihL9RSJjVdw== +ARC-Authentication-Results: i=1; phl-mx-04.messagingengine.com; + x-csa=none; + x-me-sender=fail; + x-ptr=pass smtp.helo=mail-qk1-f174.google.com + policy.ptr=mail-qk1-f174.google.com; + bimi=pass header.d=kumomta.com header.selector=default; + arc=none (no signatures found); + dkim=pass (1024-bit rsa key sha256) header.d=kumomta.com + header.i=@kumomta.com header.b=ZUrWtUUp header.a=rsa-sha256 + header.s=gw1024; + dmarc=pass policy.published-domain-policy=reject + policy.applied-disposition=none policy.evaluated-disposition=none + (p=reject,d=none,d.eval=none) policy.policy-from=p + header.from=kumomta.com; + iprev=pass smtp.remote-ip=209.85.222.174 (mail-qk1-f174.google.com); + spf=pass smtp.mailfrom=wez@kumomta.com + smtp.helo=mail-qk1-f174.google.com +X-ME-Authentication-Results: phl-mx-04.messagingengine.com; + x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 + smtp.bits=256/256; + x-vs=clean score=0 state=0 +Authentication-Results: phl-mx-04.messagingengine.com; + x-csa=none; + x-me-sender=fail; + x-ptr=pass smtp.helo=mail-qk1-f174.google.com + policy.ptr=mail-qk1-f174.google.com +Authentication-Results: phl-mx-04.messagingengine.com; + bimi=pass header.d=kumomta.com header.selector=default +Authentication-Results: phl-mx-04.messagingengine.com; + arc=none (no signatures found) +Authentication-Results: phl-mx-04.messagingengine.com; + dkim=pass (1024-bit rsa key sha256) header.d=kumomta.com + header.i=@kumomta.com header.b=ZUrWtUUp header.a=rsa-sha256 + header.s=gw1024; + dmarc=pass policy.published-domain-policy=reject + policy.applied-disposition=none policy.evaluated-disposition=none + (p=reject,d=none,d.eval=none) policy.policy-from=p + header.from=kumomta.com; + iprev=pass smtp.remote-ip=209.85.222.174 (mail-qk1-f174.google.com); + spf=pass smtp.mailfrom=wez@kumomta.com + smtp.helo=mail-qk1-f174.google.com +BIMI-Indicator: PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Inllcy + I/Pgo8c3ZnIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB2ZXJzaW9uPSIxLjIiIHZpZXdCb3g9 + IjAgMCAxMDgwIDEwODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+Ci + AgPHRpdGxlPmJpbWktc3ZnLXRpbnktMTItcHM8L3RpdGxlPgogIDxwYXRoIGlkPSJPcmFu + Z2VfQnViYmxlIiBkPSJNMjA0LjM5LDMyOS4yMWMtLjA1LTgxLjAyLDY1LjU5LTE0Ni43My + wxNDYuNjEtMTQ2Ljc3bDU1NC4wNy0uMzFjODEuMDItLjA1LDE0Ni43Myw2NS41OSwxNDYu + NzcsMTQ2LjYxbC4xNiwyOTMuMzhjLjA0LDgwLjk2LTY1LjY1LDE0Ni43My0xNDYuNjEsMT + Q2Ljc3bDUwLjk2LDE0OS43MWMyLjIzLDYuNTUtNS4xNCwxMi4xOS0xMC44OCw4LjMybC0y + MzQuMzQtMTU3LjkyLTM1OS44LjJjLTgxLjAxLjA1LTE0Ni43Mi02NS41OS0xNDYuNzctMT + Q2LjYxbC0uMTYtMjkzLjM4WiIgZmlsbD0iI2Y4YTYyOCIgZmlsbC1ydWxlPSJldmVub2Rk + IiBzdHJva2U9IiNmOGE2MjgiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZH + RoPSIxNiIvPgogIDxwYXRoIGlkPSJFbnZlbG9wZSIgZD0iTTg4OS44OSw0MzRjMC04MS4y + OS02NS45LTE0Ny4xOS0xNDcuMTktMTQ3LjE5SDE4OS42M2MtODEuMywwLTE0Ny4yLDY1Lj + ktMTQ3LjIsMTQ3LjJ2MjkyLjg3YzAsODAuNzMsNjUuMzUsMTQ2LjMxLDE0NS45OSwxNDYu + NjkuMzQsMCwuNTkuMzQuNDguNjZsLTU3LjA3LDE2Ny45NmMtLjE2LjQ2LjM2Ljg2Ljc2Lj + U4bDI1MC42Ny0xNjkuMTJjLjA4LS4wNi4xOC0uMDkuMjgtLjA5aDM1OS4xNGM4MS4zLDAs + MTQ3LjItNjUuOSwxNDcuMi0xNDcuMnYtMjkyLjM3WiIgZmlsbD0iI2ZmZiIgZmlsbC1ydW + xlPSJldmVub2RkIiBzdHJva2U9IiNmMTg5MTUiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIg + c3Ryb2tlLXdpZHRoPSI0MCIvPgogIDxwYXRoIGlkPSJUb3BfQXJyb3ciIGQ9Ik0xMDguMD + IsMjQxLjA3czYzLjcxLTEyMy41OSwxNDYtMTQ5Ljc1YzcyLjg4LTIzLjE2LDI4OS42OC0x + My4wOSwyODkuNjgtMTMuMDlsLTM5LjE1LTQ0LjIzLDM5LjE1LDQ0LjIzLTM0LjMsMzguMz + QiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Y2Yzk2MiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5k + IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQwIi8+CiAgPHBhdG + ggaWQ9IkJvdHRvbV9BcnJvdyIgZD0iTTgyNi4xMiw5NjQuOTljLTcyLjg4LDIzLjE2LTM1 + Mi4xNCwxNS43Mi0zNTIuMTQsMTUuNzJsMzkuMTUsNDQuMjQtMzkuMTUtNDQuMjQsMzQuMy + 0zOC4zNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZjZjOTYyIiBzdHJva2UtbGluZWNhcD0i + cm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNDAiLz4KIC + A8cG9seWxpbmUgaWQ9IkVudmVsb3BlX0xpZCIgcG9pbnRzPSI4Ny45OSAzMzEuNzUgNDYx + LjMzIDY3NC40MSA4NDcuNzcgMzMxLjA4IiBmaWxsPSJub25lIiBzdHJva2U9IiNmMTg5MT + UiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ry + b2tlLXdpZHRoPSI0MCIvPgo8L3N2Zz4= +BIMI-Location: v=BIMI1; + l=https://kumomta.com/hubfs/bimi-svg-tiny-12-ps.svg +X-ME-VSCause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggdduieeivdejucetufdoteggodetrf + dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu + rghilhhouhhtmecufedttdenucenucfjughrpefoggffhffvkffutgfgsehtjeertdertd + dtnecuhfhrohhmpedfhggviicuhfhurhhlohhnghdfuceofigviieskhhumhhomhhtrgdr + tghomheqnecuggftrfgrthhtvghrnhepfffgtdfhgfeiheefteeghfeklefggfetheehle + duieeltdekueeivdduheeukeefnecukfhppedvtdelrdekhedrvddvvddrudejgedpuddt + fedrudeikedrudejvddrvddtudenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmh + epihhnvghtpedvtdelrdekhedrvddvvddrudejgedphhgvlhhopehmrghilhdqqhhkuddq + fhdujeegrdhgohhoghhlvgdrtghomhdpmhgrihhlfhhrohhmpeeofigviieskhhumhhomh + htrgdrtghomheqpdhnsggprhgtphhtthhopedupdhrtghpthhtohepoeifvgiiseifvgii + fhhurhhlohhnghdrohhrgheq +X-ME-VSScore: 0 +X-ME-VSCategory: clean +X-ME-CSA: none +X-ME-Received: +X-ME-Received: +Received-SPF: pass + (kumomta.com: Sender is authorized to use 'wez@kumomta.com' in 'mfrom' identity (mechanism 'include:_spf.google.com' matched)) + receiver=phl-mx-04.messagingengine.com; + identity=mailfrom; + envelope-from="wez@kumomta.com"; + helo=mail-qk1-f174.google.com; + client-ip=209.85.222.174 +Received: from mail-qk1-f174.google.com (mail-qk1-f174.google.com [209.85.222.174]) + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) + key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) + (No client certificate requested) + by phl-mx-04.messagingengine.com (Postfix) with ESMTPS id 191D31380068 + for ; Thu, 30 Oct 2025 05:42:57 -0400 (EDT) +Received: by mail-qk1-f174.google.com with SMTP id af79cd13be357-8a9b1cb735bso80565385a.1 + for ; Thu, 30 Oct 2025 02:42:57 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=kumomta.com; s=gw1024; t=1761817375; x=1762422175; darn=wezfurlong.org; + h=content-transfer-encoding:subject:message-id:to:from:date + :mime-version:feedback-id:from:to:cc:subject:date:message-id + :reply-to; + bh=B4Sdwm/LsvO9X1e98hS643RXXxvU5oFkgjJHmUF8s3k=; + b=ZUrWtUUpDLtUvfj2pBptKB+fhkGJyYOup7W3u9f1AQCZMplp+gZF29+d8Mpej01bgt + YwBrL3tVYThSNr9lbCGfgxU0yFMAZJqsGmPqibtbOOAwHYLariRqVvwCAbfIBsOqdFbp + SflMWfIv04xyfmc9J+NIxjglWuFq8avQMWDQc= +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20230601; t=1761817375; x=1762422175; + h=content-transfer-encoding:subject:message-id:to:from:date + :mime-version:feedback-id:x-gm-message-state:from:to:cc:subject:date + :message-id:reply-to; + bh=B4Sdwm/LsvO9X1e98hS643RXXxvU5oFkgjJHmUF8s3k=; + b=fY9bkPqG3Lp877M8ge2aaX5GvdOnE8FxQCrJ++8gf9Rg7i3d9QvhaGoS4K23sGgcji + dCGlwZH1XRzb+8uIe3vXSjBafz+YAo1QvP83ATA1IZ9Bx277Ir6x3rOGbkmt8G03ZDpq + MJeQ0I/swrKGAcum2nUDBxQVdAEoRrXGWumIJyfZHDhfBu40qHzPOYkoOuxB3C9RMQeE + r6FKEqcYJJ367HBnlt+DtQCPw4hwxjHmiC1AS6NcYUR379VhPMLpnZvnDVo0hvYoxzgw + /0JF6Ms6/28Vwq4MwPeTyj90qbYwsHUoN5jTc11twGEPoVf5tCm7p9NbYrTSjGL2BYGx + DZ9Q== +X-Gm-Message-State: AOJu0YwxTPh5798JYSLr03+uMmd2skNm2eU20oA1hYLShW4imaKmP9zt + h34sHKaKcQmA6gKu5RJ3WbXrWptFGl46n6CbKkWLuiH1ynfbpeVUKcnvTCfvrAeadJ8LPtxv7t8 + OIiWV +X-Gm-Gg: ASbGncs4LvTcjUTYbJxeMzeDS2YKSVJrBMHsqoKLsfrMjOXAnOKPK43tSxuocGm/Y7N + qp4R9JImOxho5wATSsOFrjj/Sk5EVxiAZrKtcB4I9NCP0g/vuwKHNGkFJFGDeR+F0k4YKe4RIL8 + Akr+ylkKbwy9JV2r2ghlpBVbJMygyFqLJqEvCNIaXdGKd+AgW1hEriN7h9G9UQ3c1hDVqVQsISF + PDlYuxyxscyb1jbZd9Ydg96D3RhJLY/KGb8zj4R0Xmt6IyEeF5xqnTWZcuEFZmgUZAZh8FdT2sh + QyK2uHaI+I+ubPaB5AN8FFjWHVeXIkYOpyXO6iKtAl3oUrePJaT34kOYvfX9fGUlQqywLZduRjN + GJ0+hgGOqijVM2ImUhBmbpgpFWox99Oqvb0qkyjoGRJ7EknwcM5w6u3nmjtxL4aPaOm75WL5aMB + si1kc7bQGVD+xCC/9Jcng/ClWnZxs1aUPSjUcu8+DL6ymwgUvlpzSrFwPsO4A= +X-Google-Smtp-Source: AGHT+IHc20q+08CNfpTLtnwhY38hLyGsYAxu380LFBLlj0ZePkf/hBLRiNhju9rAxFY0JhEY+H8tWw== +X-Received: by 2002:a05:620a:394f:b0:8a2:a5b2:e3bc with SMTP id af79cd13be357-8aa2ea0cac4mr326831385a.89.1761817375491; + Thu, 30 Oct 2025 02:42:55 -0700 (PDT) +Received: from fauth-a2-smtp.messagingengine.com (fauth-a2-smtp.messagingengine.com. [103.168.172.201]) + by smtp.gmail.com with ESMTPSA id af79cd13be357-89f26073cedsm1231115385a.52.2025.10.30.02.42.55 + for + (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); + Thu, 30 Oct 2025 02:42:55 -0700 (PDT) +Received: from phl-compute-01.internal (phl-compute-01.internal [10.202.2.41]) + by mailfauth.phl.internal (Postfix) with ESMTP id E6F78F40074 + for ; Thu, 30 Oct 2025 05:42:54 -0400 (EDT) +Received: from phl-imap-15 ([10.202.2.104]) + by phl-compute-01.internal (MEProxy); Thu, 30 Oct 2025 05:42:54 -0400 +X-ME-Sender: + +X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeeffedrtdeggdduieeivdekucetufdoteggodetrf + dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu + rghilhhouhhtmecufedttdenucenucfjughrpefoggffhffvkffutgfgsehtjeertdertd + dtnecuhfhrohhmpedfhggviicuhfhurhhlohhnghdfuceofigviieskhhumhhomhhtrgdr + tghomheqnecuggftrfgrthhtvghrnhepfffgtdfhgfeiheefteeghfeklefggfetheehle + duieeltdekueeivdduheeukeefnecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghm + pehmrghilhhfrhhomhepfigviidomhgvshhmthhprghuthhhphgvrhhsohhnrghlihhthi + dqudehheekvddvvdeifedquddvtdehfeejvdehqdifvgiipeepkhhumhhomhhtrgdrtgho + mhesfigviihfuhhrlhhonhhgrdhorhhgpdhnsggprhgtphhtthhopedupdhmohguvgepsh + hmthhpohhuthdprhgtphhtthhopeifvgiiseifvgiifhhurhhlohhnghdrohhrgh +X-ME-Proxy: + + + + +Feedback-ID: i9d6e48e7:Fastmail +Received: by mailuser.phl.internal (Postfix, from userid 501) + id B9A12780076; Thu, 30 Oct 2025 05:42:54 -0400 (EDT) +X-Mailer: MessagingEngine.com Webmail Interface +MIME-Version: 1.0 +Date: Thu, 30 Oct 2025 09:42:34 +0000 +From: "Wez Furlong" +To: "Wez Furlong" +Message-Id: <1a30158a-fd9e-4a2c-a052-362856fec025@app.fastmail.com> +Subject: testing to see how arc turns out +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +This is a test diff --git a/crates/dkim/test/keys/2022.txt b/crates/dkim/test/keys/2022.txt deleted file mode 100644 index d7ff9ae5..00000000 --- a/crates/dkim/test/keys/2022.txt +++ /dev/null @@ -1,3 +0,0 @@ -2022._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; t=y:s; " - "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyrnZAH3hf+hp53o5gz7CfRNHme6iCW8koRNgV3bDiZcPxoC9nhjyMPWD/rizalhykziEaz0WBodeSalGjTXqH6yrlUobekxJO9UmzKrIpWCfsdbHLfTHCO6kk4JLeKs+hRs+/v2tPvcVnGD/A76cBXI5ksfrtUzeTlsPDYDSbafgBXvi9CTMAEUd3iB+HtjQbNuQJbNnZrLotB" - "PGjuFTcUKCafCmFu31K6ZMDnOJadfoZO8cClti53V2DLz7NDO3kZIGiAHsNcptcZN3MnHRhMl2Buy5vdi4lfDXhjl5ozhb8MeY0LAJikJm9RUQ3GcHBdvqchnz53gcNXIApMuK2QIDAQAB" ) ; ----- DKIM key 2022 for example.com diff --git a/crates/mailparsing/src/rfc5322_parser.rs b/crates/mailparsing/src/rfc5322_parser.rs index 521269fc..ff15d494 100644 --- a/crates/mailparsing/src/rfc5322_parser.rs +++ b/crates/mailparsing/src/rfc5322_parser.rs @@ -1058,7 +1058,7 @@ fn keyword(input: Span) -> IResult { context( "keyword", map( - take_while1(|c: char| c.is_ascii_alphanumeric() || c == '+'), + take_while1(|c: char| c.is_ascii_alphanumeric() || c == '+' || c == '-'), |s: Span| s.to_string(), ), )(input)