diff --git a/Cargo.lock b/Cargo.lock index 19024475..4b983026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4393,11 +4393,13 @@ version = "0.11.0" dependencies = [ "aws-lc-rs", "flate2", + "getrandom 0.2.17", + "getrandom 0.4.3", "hashify", "hickory-resolver", "mail-builder", "mail-parser", - "quick-xml 0.40.1", + "quick-xml 0.41.0", "rand 0.8.6", "rkyv", "rsa", @@ -5926,6 +5928,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quick_cache" version = "0.6.23" @@ -6811,9 +6822,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index 234a2ed5..aa3fd414 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -15,7 +15,7 @@ use crate::{ DomainCache, EmailAddress, EmailAddressRef, EmailCache, MailingListCache, PermissionsGroup, RECOVERY_ADMIN_ID, RoleCache, TenantCache, permissions::BuildPermissions, }, - config::smtp::auth::DkimSigner, + config::smtp::auth::DkimSigners, expr::if_block::BootstrapExprExt, network::mta::AddressResolver, storage::{ @@ -839,7 +839,7 @@ impl Server { } } - pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { + pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { let Some(domain) = self.domain(domain).await? else { return Ok(None); }; @@ -868,26 +868,24 @@ impl Server { .equal(Property::DomainId, domain.id), ) .await?; - let mut signatures = Vec::with_capacity(ids.len()); + let domain_name = &domain.names[0]; + let mut signers = DkimSigners { + dkim1: Vec::with_capacity(ids.len()), + dkim2: None, + }; for id in ids { if let Some(signature) = self.registry().object::(id).await? && matches!(signature.stage(), DkimRotationStage::Active) + && let Err(err) = signers.insert(domain_name.to_string(), signature).await { - match DkimSigner::new(domain.names[0].to_string(), signature).await { - Ok(signer) => signatures.push(signer), - Err(err) => { - trc::error!( - err.ctx(trc::Key::Id, id.id()).caused_by(trc::location!()) - ); - } - } + trc::error!(err.ctx(trc::Key::Id, id.id()).caused_by(trc::location!())); } } - if !signatures.is_empty() { - let signatures: Arc<[DkimSigner]> = signatures.into(); - let _ = guard.insert(signatures.clone()); - Ok(Some(signatures)) + if !signers.dkim1.is_empty() || signers.dkim2.is_some() { + let signers = Arc::new(signers); + let _ = guard.insert(signers.clone()); + Ok(Some(signers)) } else { Ok(None) } diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 86f71391..1fc1e8ab 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -13,7 +13,7 @@ use crate::{ mailstore::spamfilter::SpamClassifier, server::tls::parse_certificates, smtp::{ - auth::DkimSigner, + auth::DkimSigners, resolver::{Policy, Tlsa}, }, }, @@ -164,7 +164,7 @@ impl Caches { ), dkim_signers: Cache::new( cache.dkim_signatures, - (std::mem::size_of::() + 255) as u64, + (std::mem::size_of::() + 255) as u64, ), dns_txt: CacheWithTtl::new(cache.dns_txt, (std::mem::size_of::() + 255) as u64), dns_mx: CacheWithTtl::new(cache.dns_mx, ((std::mem::size_of::() + 255) * 2) as u64), diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index e4f313b1..55805311 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -11,15 +11,16 @@ use crate::expr::{ use mail_auth::{ common::crypto::{Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey}, dkim::{Canonicalization, Done}, + dkim2::{Dkim2Signer, Done as Dkim2Done, Flag}, }; use mail_parser::decoders::base64::base64_decode; use registry::{ schema::{ - enums::{self, ExpressionConstant}, + enums::{self, Dkim2Flag, ExpressionConstant}, prelude::ObjectType, structs::{Dkim1Signature, DkimSignature, SenderAuth}, }, - types::ObjectImpl, + types::{ObjectImpl, map::Map}, }; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject}; use store::registry::bootstrap::Bootstrap; @@ -71,14 +72,15 @@ pub enum VerifyStrategy { Disable, } -pub enum DkimSigner { +pub enum Dkim1Signer { RsaSha256(mail_auth::dkim::DkimSigner, Done>), Ed25519Sha256(mail_auth::dkim::DkimSigner), } -pub enum ArcSealer { - RsaSha256(mail_auth::arc::ArcSealer, Done>), - Ed25519Sha256(mail_auth::arc::ArcSealer), +#[derive(Default)] +pub struct DkimSigners { + pub dkim1: Vec, + pub dkim2: Option>, } impl MailAuthConfig { @@ -123,8 +125,8 @@ impl MailAuthConfig { } } -impl DkimSigner { - pub async fn new(domain: String, signature: DkimSignature) -> trc::Result { +impl DkimSigners { + pub async fn insert(&mut self, domain: String, signature: DkimSignature) -> trc::Result<()> { let mut errors = vec![]; if !signature.validate(&mut errors) { return Err(trc::DkimEvent::BuildError @@ -156,9 +158,10 @@ impl DkimSigner { .details("Failed to build ED25519 key") })?; - Ok(DkimSigner::Ed25519Sha256(build_dkim1_signer( - domain, signature, key, - ))) + self.dkim1 + .push(Dkim1Signer::Ed25519Sha256(build_dkim1_signer( + domain, signature, key, + ))); } DkimSignature::Dkim1RsaSha256(signature) => { let private_key = signature @@ -168,17 +171,70 @@ impl DkimSigner { .map_err(|err| trc::DkimEvent::BuildError.reason(err))?; let key = rsa_key_parse(private_key.as_bytes())?; - Ok(DkimSigner::RsaSha256(build_dkim1_signer( + self.dkim1.push(Dkim1Signer::RsaSha256(build_dkim1_signer( domain, signature, key, - ))) + ))); } - DkimSignature::Dkim2Ed25519Sha256(_) | DkimSignature::Dkim2RsaSha256(_) => { - todo!() + DkimSignature::Dkim2Ed25519Sha256(signature) => { + let private_key = signature + .private_key + .secret() + .await + .map_err(|err| trc::DkimEvent::BuildError.reason(err))?; + let private_key = simple_pem_parse(&private_key).ok_or_else(|| { + trc::DkimEvent::BuildError + .reason("Failed to parse ED25519 private key PEM") + .details("Invalid PEM format") + })?; + let key = + Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build ED25519 key") + })?; + + self.dkim2 = Some(match self.dkim2.take() { + None => Dkim2Signer::from_key(key) + .domain(domain) + .selector(signature.selector) + .flags(map_dkim2_flags(signature.flags)), + Some(signer) => signer + .additional_key(key, signature.selector) + .flags(map_dkim2_flags(signature.flags)), + }); + } + DkimSignature::Dkim2RsaSha256(signature) => { + let private_key = signature + .private_key + .secret() + .await + .map_err(|err| trc::DkimEvent::BuildError.reason(err))?; + let key = rsa_key_parse(private_key.as_bytes())?; + + self.dkim2 = Some(match self.dkim2.take() { + None => Dkim2Signer::from_key(key) + .domain(domain) + .selector(signature.selector) + .flags(map_dkim2_flags(signature.flags)), + Some(signer) => signer + .additional_key(key, signature.selector) + .flags(map_dkim2_flags(signature.flags)), + }); } } + + Ok(()) } } +fn map_dkim2_flags(flags: Map) -> impl Iterator { + flags.into_inner().into_iter().map(|flag| match flag { + Dkim2Flag::Donotmodify => Flag::DoNotModify, + Dkim2Flag::Donotexplode => Flag::DoNotExplode, + Dkim2Flag::Feedback => Flag::Feedback, + }) +} + pub fn rsa_key_parse(private_key: &[u8]) -> trc::Result> { PrivatePkcs1KeyDer::from_pem_slice(private_key) .map(PrivateKeyDer::Pkcs1) @@ -309,8 +365,16 @@ impl VerifyStrategy { } } -impl CacheItemWeight for DkimSigner { +impl CacheItemWeight for Dkim1Signer { fn weight(&self) -> u64 { std::mem::size_of::() as u64 } } + +impl CacheItemWeight for DkimSigners { + fn weight(&self) -> u64 { + (std::mem::size_of::() + + self.dkim1.len() * std::mem::size_of::() + + std::mem::size_of::>()) as u64 + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index f200e83a..eca290f9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -18,7 +18,7 @@ use crate::{ scripts::Scripting, spamfilter::{IpResolver, SpamClassifier, SpamFilterConfig}, }, - smtp::auth::DkimSigner, + smtp::auth::DkimSigners, }, ipc::TrainTaskController, network::security::BlockedIps, @@ -193,7 +193,7 @@ pub struct Caches { pub tenants: Cache>, pub lists: Cache>, - pub dkim_signers: Cache>, + pub dkim_signers: Cache>, pub dns_txt: CacheWithTtl, Txt>, pub dns_mx: CacheWithTtl, RecordSet>, diff --git a/crates/common/src/network/mta.rs b/crates/common/src/network/mta.rs index 413337eb..7c4ed99e 100644 --- a/crates/common/src/network/mta.rs +++ b/crates/common/src/network/mta.rs @@ -10,7 +10,7 @@ use crate::{ config::{ mailstore::spamfilter::SpamClassifier, smtp::{ - auth::DkimSigner, + auth::DkimSigners, queue::{ ConnectionStrategy, DEFAULT_QUEUE_NAME, MxConfig, QueueExpiry, QueueName, QueueStrategy, RequireOptional, RoutingStrategy, TlsStrategy, VirtualQueue, @@ -188,7 +188,7 @@ impl Server { &self, domain: &str, session_id: u64, - ) -> trc::Result>> { + ) -> trc::Result>> { if let Some(signers) = self.dkim_signers(domain).await? { Ok(Some(signers)) } else { diff --git a/crates/http/src/api/diagnose.rs b/crates/http/src/api/diagnose.rs index 9c11e2e1..43827754 100644 --- a/crates/http/src/api/diagnose.rs +++ b/crates/http/src/api/diagnose.rs @@ -296,7 +296,9 @@ async fn delivery_diagnose( Err(err) => { if matches!( &err, - smtp::outbound::mta_sts::Error::Dns(mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) + smtp::outbound::mta_sts::Error::Dns(mail_auth::Error::Dns( + mail_auth::DnsError::RecordNotFound(_) + )) ) { tx.send(DeliveryStage::MtaStsNotFound { elapsed: now.elapsed_ms(), @@ -346,7 +348,10 @@ async fn delivery_diagnose( .await?; } Err(err) => { - if matches!(&err, mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) { + if matches!( + &err, + mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_)) + ) { tx.send(DeliveryStage::TlsRptNotFound { elapsed: now.elapsed_ms(), }) @@ -426,7 +431,10 @@ async fn delivery_diagnose( None } Err(err) => { - if matches!(&err, mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) { + if matches!( + &err, + mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_)) + ) { tx.send(DeliveryStage::TlsaNotFound { elapsed: now.elapsed_ms(), reason: "No TLSA records found for MX".to_string(), diff --git a/crates/jmap/src/registry/mapping/action.rs b/crates/jmap/src/registry/mapping/action.rs index 8e1fae14..c865a29c 100644 --- a/crates/jmap/src/registry/mapping/action.rs +++ b/crates/jmap/src/registry/mapping/action.rs @@ -14,8 +14,8 @@ use common::{ use jmap_proto::error::set::{SetError, SetErrorType}; use jmap_tools::{JsonPointer, Key}; use mail_auth::{ - AuthenticatedMessage, DkimResult, DmarcResult, dmarc::verify::DmarcParameters, - spf::verify::SpfParameters, + AuthenticatedMessage, Dkim2Result, DkimResult, DmarcResult, dkim2::Envelope as Dkim2Envelope, + dmarc::verify::DmarcParameters, spf::verify::SpfParameters, }; use mail_parser::MessageParser; use registry::{ @@ -158,6 +158,7 @@ pub(crate) async fn action_set( | Property::RemoteIp | Property::EhloDomain | Property::MailFrom + | Property::To ) ) }); @@ -320,6 +321,20 @@ async fn classify_spam(server: &Server, mut request: SpamClassify) -> Option Option Option Property::DisabledPermissions, b"discardAfter" => Property::DiscardAfter, b"disposition" => Property::Disposition, + b"dkim2Pass" => Property::Dkim2Pass, + b"dkim2Result" => Property::Dkim2Result, b"dkimAdspDns" => Property::DkimAdspDns, b"dkimCanonicalizedBody" => Property::DkimCanonicalizedBody, b"dkimCanonicalizedHeader" => Property::DkimCanonicalizedHeader, @@ -1537,6 +1539,8 @@ impl EnumImpl for Property { Property::DisabledPermissions => "disabledPermissions", Property::DiscardAfter => "discardAfter", Property::Disposition => "disposition", + Property::Dkim2Pass => "dkim2Pass", + Property::Dkim2Result => "dkim2Result", Property::DkimAdspDns => "dkimAdspDns", Property::DkimCanonicalizedBody => "dkimCanonicalizedBody", Property::DkimCanonicalizedHeader => "dkimCanonicalizedHeader", @@ -2462,6 +2466,8 @@ impl EnumImpl for Property { 629 => Some(Property::DisabledPermissions), 872 => Some(Property::DiscardAfter), 747 => Some(Property::Disposition), + 917 => Some(Property::Dkim2Pass), + 916 => Some(Property::Dkim2Result), 83 => Some(Property::DkimAdspDns), 84 => Some(Property::DkimCanonicalizedBody), 85 => Some(Property::DkimCanonicalizedHeader), @@ -3173,7 +3179,7 @@ impl EnumImpl for Property { } } - const COUNT: usize = 916; + const COUNT: usize = 918; } impl serde::Serialize for Property { diff --git a/crates/registry/src/schema/structs.rs b/crates/registry/src/schema/structs.rs index 1d796b97..252b9623 100644 --- a/crates/registry/src/schema/structs.rs +++ b/crates/registry/src/schema/structs.rs @@ -1225,6 +1225,8 @@ pub struct DmarcTroubleshoot { pub ehlo_domain: String, #[serde(rename = "mailFrom")] pub mail_from: String, + #[serde(rename = "to")] + pub to: Map, #[serde(rename = "message")] pub message: Option, #[serde(rename = "spfEhloDomain")] @@ -1243,6 +1245,10 @@ pub struct DmarcTroubleshoot { pub dkim_results: List, #[serde(rename = "dkimPass")] pub dkim_pass: bool, + #[serde(rename = "dkim2Result")] + pub dkim2_result: DmarcTroubleshootAuthResult, + #[serde(rename = "dkim2Pass")] + pub dkim2_pass: bool, #[serde(rename = "arcResult")] pub arc_result: DmarcTroubleshootAuthResult, #[serde(rename = "dmarcResult")] diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 42dd04a4..6016f757 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -9339,6 +9339,12 @@ impl DmarcTroubleshoot { if value.is_empty() { errors.push(ValidationError::required(Property::MailFrom)); } + let value = &self.to; + for value in value.iter() { + if value.is_empty() { + errors.push(ValidationError::required(Property::To)); + } + } if let Some(value) = &self.message { if value.is_empty() { errors.push(ValidationError::required(Property::Message)); @@ -9368,6 +9374,8 @@ impl DmarcTroubleshoot { for value in value.values() { value.validate(errors); } + let value = &self.dkim2_result; + value.validate(errors); let value = &self.arc_result; value.validate(errors); let value = &self.dmarc_result; @@ -9381,6 +9389,7 @@ impl Pickle for DmarcTroubleshoot { self.remote_ip.pickle(out); self.ehlo_domain.pickle(out); self.mail_from.pickle(out); + self.to.pickle(out); self.message.pickle(out); self.spf_ehlo_domain.pickle(out); self.spf_ehlo_result.pickle(out); @@ -9390,6 +9399,8 @@ impl Pickle for DmarcTroubleshoot { self.ip_rev_ptr.pickle(out); self.dkim_results.pickle(out); self.dkim_pass.pickle(out); + self.dkim2_result.pickle(out); + self.dkim2_pass.pickle(out); self.arc_result.pickle(out); self.dmarc_result.pickle(out); self.dmarc_pass.pickle(out); @@ -9402,6 +9413,7 @@ impl Pickle for DmarcTroubleshoot { this.remote_ip = Pickle::unpickle(stream)?; this.ehlo_domain = Pickle::unpickle(stream)?; this.mail_from = Pickle::unpickle(stream)?; + this.to = Pickle::unpickle(stream)?; this.message = Pickle::unpickle(stream)?; this.spf_ehlo_domain = Pickle::unpickle(stream)?; this.spf_ehlo_result = Pickle::unpickle(stream)?; @@ -9411,6 +9423,8 @@ impl Pickle for DmarcTroubleshoot { this.ip_rev_ptr = Pickle::unpickle(stream)?; this.dkim_results = Pickle::unpickle(stream)?; this.dkim_pass = Pickle::unpickle(stream)?; + this.dkim2_result = Pickle::unpickle(stream)?; + this.dkim2_pass = Pickle::unpickle(stream)?; this.arc_result = Pickle::unpickle(stream)?; this.dmarc_result = Pickle::unpickle(stream)?; this.dmarc_pass = Pickle::unpickle(stream)?; @@ -9426,6 +9440,7 @@ impl Default for DmarcTroubleshoot { remote_ip: Default::default(), ehlo_domain: Default::default(), mail_from: Default::default(), + to: Default::default(), message: Default::default(), spf_ehlo_domain: Default::default(), spf_ehlo_result: Default::default(), @@ -9435,6 +9450,8 @@ impl Default for DmarcTroubleshoot { ip_rev_ptr: Default::default(), dkim_results: Default::default(), dkim_pass: false, + dkim2_result: Default::default(), + dkim2_pass: false, arc_result: Default::default(), dmarc_result: Default::default(), dmarc_pass: false, @@ -9446,10 +9463,11 @@ impl Default for DmarcTroubleshoot { impl IntoValue for DmarcTroubleshoot { fn into_value(self) -> JmapValue<'static> { - let mut map = jmap_tools::Map::with_capacity(19); + let mut map = jmap_tools::Map::with_capacity(22); map.insert_unchecked(Property::RemoteIp, self.remote_ip.into_value()); map.insert_unchecked(Property::EhloDomain, self.ehlo_domain.into_value()); map.insert_unchecked(Property::MailFrom, self.mail_from.into_value()); + map.insert_unchecked(Property::To, self.to.into_value()); map.insert_unchecked(Property::Message, self.message.into_value()); map.insert_unchecked(Property::SpfEhloDomain, self.spf_ehlo_domain.into_value()); map.insert_unchecked(Property::SpfEhloResult, self.spf_ehlo_result.into_value()); @@ -9465,6 +9483,8 @@ impl IntoValue for DmarcTroubleshoot { map.insert_unchecked(Property::IpRevPtr, self.ip_rev_ptr.into_value()); map.insert_unchecked(Property::DkimResults, self.dkim_results.into_value()); map.insert_unchecked(Property::DkimPass, self.dkim_pass.into_value()); + map.insert_unchecked(Property::Dkim2Result, self.dkim2_result.into_value()); + map.insert_unchecked(Property::Dkim2Pass, self.dkim2_pass.into_value()); map.insert_unchecked(Property::ArcResult, self.arc_result.into_value()); map.insert_unchecked(Property::DmarcResult, self.dmarc_result.into_value()); map.insert_unchecked(Property::DmarcPass, self.dmarc_pass.into_value()); @@ -9486,6 +9506,9 @@ impl RegistryJsonPropertyPatch for DmarcTroubleshoot { Some(Property::MailFrom) => self .mail_from .patch(pointer.with_validators(&[StringValidator::Email]), value), + Some(Property::To) => self + .to + .patch(pointer.with_validators(&[StringValidator::Email]), value), Some(Property::Message) => self.message.patch(pointer, value), Some(Property::SpfEhloDomain) => self.spf_ehlo_domain.patch(pointer, value), Some(Property::SpfEhloResult) => pointer.assert_server_set(), @@ -9495,6 +9518,8 @@ impl RegistryJsonPropertyPatch for DmarcTroubleshoot { Some(Property::IpRevPtr) => pointer.assert_server_set(), Some(Property::DkimResults) => pointer.assert_server_set(), Some(Property::DkimPass) => pointer.assert_server_set(), + Some(Property::Dkim2Result) => pointer.assert_server_set(), + Some(Property::Dkim2Pass) => pointer.assert_server_set(), Some(Property::ArcResult) => pointer.assert_server_set(), Some(Property::DmarcResult) => pointer.assert_server_set(), Some(Property::DmarcPass) => pointer.assert_server_set(), diff --git a/crates/registry/src/utils/report.rs b/crates/registry/src/utils/report.rs index 87c4de71..0bac6411 100644 --- a/crates/registry/src/utils/report.rs +++ b/crates/registry/src/utils/report.rs @@ -897,6 +897,30 @@ impl From<&mail_auth::DkimResult> for structs::DmarcTroubleshootAuthResult { } } +impl From<&mail_auth::Dkim2Result> for structs::DmarcTroubleshootAuthResult { + fn from(value: &mail_auth::Dkim2Result) -> Self { + match value { + mail_auth::Dkim2Result::Pass => structs::DmarcTroubleshootAuthResult::Pass, + mail_auth::Dkim2Result::Fail(error) => { + structs::DmarcTroubleshootAuthResult::Fail(structs::DmarcTroubleshootDetails { + details: error.to_string().into(), + }) + } + mail_auth::Dkim2Result::PermError(error) => { + structs::DmarcTroubleshootAuthResult::PermError(structs::DmarcTroubleshootDetails { + details: error.to_string().into(), + }) + } + mail_auth::Dkim2Result::TempError(error) => { + structs::DmarcTroubleshootAuthResult::TempError(structs::DmarcTroubleshootDetails { + details: error.to_string().into(), + }) + } + mail_auth::Dkim2Result::None => structs::DmarcTroubleshootAuthResult::None, + } + } +} + impl From<&mail_auth::DmarcResult> for structs::DmarcTroubleshootAuthResult { fn from(value: &mail_auth::DmarcResult) -> Self { match value { diff --git a/crates/services/src/task_manager/dkim.rs b/crates/services/src/task_manager/dkim.rs index d5673d4d..51b6b3c9 100644 --- a/crates/services/src/task_manager/dkim.rs +++ b/crates/services/src/task_manager/dkim.rs @@ -19,8 +19,8 @@ use registry::{ enums::{DkimRotationStage, DkimSignatureType, DnsRecordType}, prelude::{Object, ObjectType, Property}, structs::{ - Dkim1Signature, DkimManagement, DkimSignature, DnsManagement, Domain, SecretText, - SecretTextValue, Task, TaskDomainManagement, TaskStatus, + Dkim1Signature, Dkim2Signature, DkimManagement, DkimSignature, DnsManagement, Domain, + SecretText, SecretTextValue, Task, TaskDomainManagement, TaskStatus, }, }, types::{datetime::UTCDateTime, id::ObjectId}, @@ -148,10 +148,11 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R let secret = { if dkim.selector_template.contains("dummy") { match algorithm { - DkimSignatureType::Dkim1Ed25519Sha256 => TEST_ED25519_KEY.to_string(), - DkimSignatureType::Dkim1RsaSha256 => TEST_RSA_KEY.to_string(), - DkimSignatureType::Dkim2Ed25519Sha256 - | DkimSignatureType::Dkim2RsaSha256 => todo!(), + DkimSignatureType::Dkim1Ed25519Sha256 + | DkimSignatureType::Dkim2Ed25519Sha256 => TEST_ED25519_KEY.to_string(), + DkimSignatureType::Dkim1RsaSha256 | DkimSignatureType::Dkim2RsaSha256 => { + TEST_RSA_KEY.to_string() + } } } else { generate_dkim_private_key(algorithm).await.unwrap().unwrap() @@ -177,18 +178,38 @@ async fn dkim_management(server: &Server, task: &TaskDomainManagement) -> trc::R }; // Build key - let signature = Dkim1Signature { - stage: DkimRotationStage::Active, - domain_id: task.domain_id, - member_tenant_id: domain.member_tenant_id, - selector: selector.clone(), - private_key: SecretText::Text(SecretTextValue { secret }), - ..Default::default() - }; + let private_key = SecretText::Text(SecretTextValue { secret }); let mut signature = match algorithm { - DkimSignatureType::Dkim1Ed25519Sha256 => DkimSignature::Dkim1Ed25519Sha256(signature), - DkimSignatureType::Dkim1RsaSha256 => DkimSignature::Dkim1RsaSha256(signature), - DkimSignatureType::Dkim2Ed25519Sha256 | DkimSignatureType::Dkim2RsaSha256 => todo!(), + DkimSignatureType::Dkim1Ed25519Sha256 | DkimSignatureType::Dkim1RsaSha256 => { + let signature = Dkim1Signature { + stage: DkimRotationStage::Active, + domain_id: task.domain_id, + member_tenant_id: domain.member_tenant_id, + selector: selector.clone(), + private_key, + ..Default::default() + }; + if algorithm == DkimSignatureType::Dkim1Ed25519Sha256 { + DkimSignature::Dkim1Ed25519Sha256(signature) + } else { + DkimSignature::Dkim1RsaSha256(signature) + } + } + DkimSignatureType::Dkim2Ed25519Sha256 | DkimSignatureType::Dkim2RsaSha256 => { + let signature = Dkim2Signature { + stage: DkimRotationStage::Active, + domain_id: task.domain_id, + member_tenant_id: domain.member_tenant_id, + selector: selector.clone(), + private_key, + ..Default::default() + }; + if algorithm == DkimSignatureType::Dkim2Ed25519Sha256 { + DkimSignature::Dkim2Ed25519Sha256(signature) + } else { + DkimSignature::Dkim2RsaSha256(signature) + } + } }; // Publish key diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 44cfcbf6..ed6b075b 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -29,17 +29,18 @@ use common::{ scripts::ScriptModification, }; use mail_auth::{ - AuthenticatedMessage, AuthenticationResults, DkimResult, DmarcResult, ReceivedSpf, + AuthenticatedMessage, AuthenticationResults, Dkim2Result, DkimResult, DmarcResult, ReceivedSpf, common::{ crypto::Algorithm, headers::{Header, HeaderWriter}, verify::VerifySignature, }, dkim::DkimError, + dkim2::{Dkim2Dsn, Dkim2DsnFailure, Envelope as Dkim2Envelope}, dmarc::{self, verify::DmarcParameters}, }; use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; -use mail_parser::{MessageParser, parsers::fields::thread::thread_name}; +use mail_parser::{MessageParser, MimeHeaders, parsers::fields::thread::thread_name}; use registry::schema::structs::Rate; use sieve::{SpamStatus, runtime::Variable}; use smtp_proto::{ @@ -233,12 +234,99 @@ impl Session { None }; - // Build authentication results header + // Verify DKIM2 let mail_from = self.data.mail_from.as_ref().unwrap(); + let dkim2_output = if (dkim.verify() || dmarc.verify()) + && (!auth_message.dkim2_signatures.is_empty() || auth_message.has_dkim2_errors) + { + // Discard forged DKIM2-signed delivery status notifications + if dkim.verify() + && !auth_message.dkim2_signatures.is_empty() + && let Some(dsn) = + parse_dkim2_dsn(&parsed_message, &auth_message, raw_message.as_slice()) + && let Err(failure) = self + .server + .core + .smtp + .resolvers + .dns + .verify_dkim2_dsn( + self.server.inner.cache.build_auth_parameters(&dsn), + Dkim2Envelope { + mail_from: &mail_from.address, + rcpt_to: self.data.rcpt_to.iter().map(|r| r.address.as_str()), + }, + ) + .await + && matches!( + failure, + Dkim2DsnFailure::DsnChainFailed + | Dkim2DsnFailure::ReturnedChainFailed + | Dkim2DsnFailure::NotAligned + ) + { + trc::event!( + Smtp(SmtpEvent::Dkim2DsnDiscarded), + SpanId = self.data.session_id, + From = mail_from.address.to_string(), + To = self + .data + .rcpt_to + .iter() + .map(|rcpt| trc::Value::from(rcpt.address.to_string())) + .collect::>(), + Reason = failure.to_string(), + ); + + self.data.messages_sent += 1; + return (b"250 2.0.0 Message queued for delivery.\r\n"[..]).into(); + } + + let time = Instant::now(); + let output = self + .server + .core + .smtp + .resolvers + .dns + .verify_dkim2( + self.server.inner.cache.build_auth_parameters(&auth_message), + Dkim2Envelope { + mail_from: &mail_from.address, + rcpt_to: self.data.rcpt_to.iter().map(|r| r.address.as_str()), + }, + ) + .await; + + if !matches!(output.result(), Dkim2Result::None) { + trc::event!( + Smtp(if matches!(output.result(), Dkim2Result::Pass) { + SmtpEvent::Dkim2Pass + } else { + SmtpEvent::Dkim2Fail + }), + SpanId = self.data.session_id, + Result = trc::Error::from(&output), + Elapsed = time.elapsed(), + ); + } + + Some(output) + } else { + None + }; + + // Build authentication results header let mut auth_results = AuthenticationResults::new(&self.hostname); if !dkim_output.is_empty() { auth_results = auth_results.with_dkim_results(&dkim_output, auth_message.from()) } + if let Some(dkim2_output) = &dkim2_output + && (!matches!(dkim2_output.result(), Dkim2Result::None) + || !dkim2_output.chain().is_empty()) + { + auth_results = auth_results.with_dkim2_result(dkim2_output); + } if let Some(spf_ehlo) = &self.data.spf_ehlo { auth_results = auth_results.with_spf_ehlo_result( spf_ehlo, @@ -273,6 +361,7 @@ impl Session { DmarcParameters { message: &auth_message, dkim_output: &dkim_output, + dkim2_output: dkim2_output.as_ref(), rfc5321_mail_from_domain: if !mail_from.domain.is_empty() { &mail_from.domain } else { @@ -329,6 +418,7 @@ impl Session { rejected, dmarc_output, &dkim_output, + dkim2_output.as_ref(), &arc_output, ) .await; @@ -438,6 +528,7 @@ impl Session { .spam_classify( &parsed_message, &dkim_output, + dkim2_output.as_ref(), (&arc_output).into(), dmarc_result.as_ref(), dmarc_policy.as_ref(), @@ -688,7 +779,7 @@ impl Session { { match self.server.dkim_signers(&sign_with_domain).await { Ok(Some(signers)) => { - for signer in signers.as_ref() { + for signer in &signers.dkim1 { match signer.sign_chained(&[headers.as_ref(), raw_message]) { Ok(signature) => { signature.write_header(&mut headers); @@ -999,3 +1090,47 @@ impl Session { headers.extend_from_slice(b"\r\n"); } } + +fn parse_dkim2_dsn<'x, 'r>( + parsed_message: &mail_parser::Message<'x>, + raw: &'r AuthenticatedMessage<'x>, + raw_message: &'x [u8], +) -> Option, AuthenticatedMessage<'x>>> { + if !parsed_message.content_type().is_some_and(|ct| { + ct.ctype().eq_ignore_ascii_case("multipart") + && ct + .subtype() + .is_some_and(|subtype| subtype.eq_ignore_ascii_case("report")) + && ct + .attribute("report-type") + .is_some_and(|report_type| report_type.eq_ignore_ascii_case("delivery-status")) + }) { + return None; + } + let mail_parser::PartType::Multipart(children) = &parsed_message.root_part().body else { + return None; + }; + + let mut returned = &b""[..]; + let mut returned_full = false; + for child in children { + let part = parsed_message.parts.get(*child as usize)?; + if part.is_content_type("message", "rfc822") { + returned = raw_message.get(part.offset_body as usize..part.offset_end as usize)?; + returned_full = true; + } else if part.is_content_type("text", "rfc822-headers") { + returned = raw_message.get(part.offset_body as usize..part.offset_end as usize)?; + returned_full = false; + } + } + + if !returned.is_empty() { + Some(Dkim2Dsn::new( + raw, + AuthenticatedMessage::parse(returned)?, + returned_full, + )) + } else { + None + } +} diff --git a/crates/smtp/src/inbound/mod.rs b/crates/smtp/src/inbound/mod.rs index ff622120..ff255228 100644 --- a/crates/smtp/src/inbound/mod.rs +++ b/crates/smtp/src/inbound/mod.rs @@ -4,14 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::config::smtp::auth::Dkim1Signer; +use mail_auth::{DkimResult, DmarcResult, IprevResult, SpfResult, dkim::Signature, dmarc::Policy}; use std::borrow::Cow; -use common::config::smtp::auth::{ArcSealer, DkimSigner}; -use mail_auth::{ - ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimResult, DmarcResult, IprevResult, - SpfResult, arc::ArcSet, dkim::Signature, dmarc::Policy, -}; - pub mod auth; pub mod data; pub mod ehlo; @@ -30,45 +26,22 @@ pub struct FilterResponse { pub disconnect: bool, } -pub trait ArcSeal { - fn seal<'x>( - &self, - message: &'x AuthenticatedMessage, - results: &'x AuthenticationResults, - arc_output: &'x ArcOutput, - ) -> mail_auth::Result>; -} - -impl ArcSeal for ArcSealer { - fn seal<'x>( - &self, - message: &'x AuthenticatedMessage, - results: &'x AuthenticationResults, - arc_output: &'x ArcOutput, - ) -> mail_auth::Result> { - match self { - ArcSealer::RsaSha256(sealer) => sealer.seal(message, results, arc_output), - ArcSealer::Ed25519Sha256(sealer) => sealer.seal(message, results, arc_output), - } - } -} - pub trait DkimSign { fn sign(&self, message: &[u8]) -> mail_auth::Result; fn sign_chained(&self, message: &[&[u8]]) -> mail_auth::Result; } -impl DkimSign for DkimSigner { +impl DkimSign for Dkim1Signer { fn sign(&self, message: &[u8]) -> mail_auth::Result { match self { - DkimSigner::RsaSha256(signer) => signer.sign(message), - DkimSigner::Ed25519Sha256(signer) => signer.sign(message), + Dkim1Signer::RsaSha256(signer) => signer.sign(message), + Dkim1Signer::Ed25519Sha256(signer) => signer.sign(message), } } fn sign_chained(&self, message: &[&[u8]]) -> mail_auth::Result { match self { - DkimSigner::RsaSha256(signer) => signer.sign_chained(message.iter().copied()), - DkimSigner::Ed25519Sha256(signer) => signer.sign_chained(message.iter().copied()), + Dkim1Signer::RsaSha256(signer) => signer.sign_chained(message.iter().copied()), + Dkim1Signer::Ed25519Sha256(signer) => signer.sign_chained(message.iter().copied()), } } } diff --git a/crates/smtp/src/inbound/spam.rs b/crates/smtp/src/inbound/spam.rs index 7d059c85..0ebb6705 100644 --- a/crates/smtp/src/inbound/spam.rs +++ b/crates/smtp/src/inbound/spam.rs @@ -6,7 +6,7 @@ use crate::core::Session; use common::{config::mailstore::spamfilter::SpamFilterAction, network::SessionStream}; -use mail_auth::{ArcOutput, DkimOutput, DmarcResult, dmarc::Policy}; +use mail_auth::{ArcOutput, DkimOutput, DmarcResult, dkim2::Dkim2Output, dmarc::Policy}; use mail_parser::Message; use spam_filter::{ SpamFilterInput, @@ -21,6 +21,7 @@ impl Session { &'x self, message: &'x Message<'x>, dkim_result: &'x [DkimOutput<'x>], + dkim2_result: Option<&'x Dkim2Output<'x>>, arc_result: Option<&'x ArcOutput<'x>>, dmarc_result: Option<&'x DmarcResult>, dmarc_policy: Option<&'x Policy>, @@ -29,6 +30,7 @@ impl Session { let mut ctx = server.spam_filter_init(self.build_spam_input( message, dkim_result, + dkim2_result, arc_result, dmarc_result, dmarc_policy, @@ -47,6 +49,7 @@ impl Session { &'x self, message: &'x Message<'x>, dkim_result: &'x [DkimOutput<'x>], + dkim2_result: Option<&'x Dkim2Output<'x>>, arc_result: Option<&'x ArcOutput>, dmarc_result: Option<&'x DmarcResult>, dmarc_policy: Option<&'x Policy>, @@ -58,6 +61,7 @@ impl Session { spf_ehlo_result: self.data.spf_ehlo.as_ref(), spf_mail_from_result: self.data.spf_mail_from.as_ref(), dkim_result, + dkim2_result, dmarc_result, dmarc_policy, iprev_result: self.data.iprev.as_ref(), diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index c1c6d1b4..6a701f08 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -300,77 +300,79 @@ impl QueuedMessage { ); // Obtain TLS reporting - let tls_report = - if is_smtp && mx_config.is_some() && (message.message.flags & FROM_REPORT == 0) { - match server - .eval_if( - &server.core.smtp.report.tls.send, - &envelope, - message.span_id, - ) - .await - .unwrap_or(AggregateFrequency::Never) - { - interval @ (AggregateFrequency::Hourly - | AggregateFrequency::Daily - | AggregateFrequency::Weekly) => { - let time = Instant::now(); - match server - .core - .smtp - .resolvers - .dns - .txt_lookup::( - format!("_smtp._tls.{domain}."), - Some(&server.inner.cache.dns_txt), - ) - .await - { - Ok(record) => { - trc::event!( - TlsRpt(TlsRptEvent::RecordFetch), - SpanId = message.span_id, - Domain = domain.to_string(), - Details = record - .rua - .iter() - .map(|uri| trc::Value::from(match uri { - mail_auth::mta_sts::ReportUri::Mail(uri) - | mail_auth::mta_sts::ReportUri::Http(uri) => - uri.to_string(), - })) - .collect::>(), - Elapsed = time.elapsed(), - ); + let tls_report = if is_smtp + && mx_config.is_some() + && (message.message.flags & FROM_REPORT == 0) + { + match server + .eval_if( + &server.core.smtp.report.tls.send, + &envelope, + message.span_id, + ) + .await + .unwrap_or(AggregateFrequency::Never) + { + interval @ (AggregateFrequency::Hourly + | AggregateFrequency::Daily + | AggregateFrequency::Weekly) => { + let time = Instant::now(); + match server + .core + .smtp + .resolvers + .dns + .txt_lookup::( + format!("_smtp._tls.{domain}."), + Some(&server.inner.cache.dns_txt), + ) + .await + { + Ok(record) => { + trc::event!( + TlsRpt(TlsRptEvent::RecordFetch), + SpanId = message.span_id, + Domain = domain.to_string(), + Details = record + .rua + .iter() + .map(|uri| trc::Value::from(match uri { + mail_auth::mta_sts::ReportUri::Mail(uri) + | mail_auth::mta_sts::ReportUri::Http(uri) => + uri.to_string(), + })) + .collect::>(), + Elapsed = time.elapsed(), + ); - TlsRptOptions { record, interval }.into() - } - Err(mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) => { - trc::event!( - TlsRpt(TlsRptEvent::RecordNotFound), - SpanId = message.span_id, - Domain = domain.to_string(), - Elapsed = time.elapsed(), - ); - None - } - Err(err) => { - trc::event!( - TlsRpt(TlsRptEvent::RecordFetchError), - SpanId = message.span_id, - Domain = domain.to_string(), - CausedBy = trc::Error::from(err), - Elapsed = time.elapsed(), - ); - None - } + TlsRptOptions { record, interval }.into() + } + Err(mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) => { + trc::event!( + TlsRpt(TlsRptEvent::RecordNotFound), + SpanId = message.span_id, + Domain = domain.to_string(), + Elapsed = time.elapsed(), + ); + None + } + Err(err) => { + trc::event!( + TlsRpt(TlsRptEvent::RecordFetchError), + SpanId = message.span_id, + Domain = domain.to_string(), + CausedBy = trc::Error::from(err), + Elapsed = time.elapsed(), + ); + None } } - _ => None, } - } else { - None - }; + _ => None, + } + } else { + None + }; // Obtain MTA-STS policy for domain let mta_sts_policy = if mx_config.is_some() && tls_strategy.try_mta_sts() && is_smtp { @@ -400,7 +402,9 @@ impl QueuedMessage { let strict = tls_strategy.is_mta_sts_required(); if let Some(tls_report) = &tls_report { match &err { - mta_sts::Error::Dns(mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) => { + mta_sts::Error::Dns(mail_auth::Error::Dns( + mail_auth::DnsError::RecordNotFound(_), + )) => { if strict { server.schedule_report(TlsEvent { policy: PolicyType::Sts(None), @@ -417,7 +421,9 @@ impl QueuedMessage { .await; } } - mta_sts::Error::Dns(mail_auth::Error::Dns(mail_auth::DnsError::Resolver(_))) => (), + mta_sts::Error::Dns(mail_auth::Error::Dns( + mail_auth::DnsError::Resolver(_), + )) => (), _ => { server .schedule_report(TlsEvent { @@ -436,7 +442,9 @@ impl QueuedMessage { } match &err { - mta_sts::Error::Dns(mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))) => { + mta_sts::Error::Dns(mail_auth::Error::Dns( + mail_auth::DnsError::RecordNotFound(_), + )) => { trc::event!( MtaSts(MtaStsEvent::PolicyNotFound), SpanId = message.span_id, @@ -821,8 +829,12 @@ impl QueuedMessage { None } Err(err) => { - let not_found = - matches!(&err, mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound(_))); + let not_found = matches!( + &err, + mail_auth::Error::Dns(mail_auth::DnsError::RecordNotFound( + _ + )) + ); if not_found { trc::event!( diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index c86aa6cd..d46f4fd4 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -21,6 +21,7 @@ use mail_auth::{ ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput, SpfResult, common::verify::VerifySignature, + dkim2::Dkim2Output, dmarc::{self}, report::{AuthFailureType, IdentityAlignment, PolicyPublished, Record, SPFDomainScope}, }; @@ -50,6 +51,7 @@ impl Session { rejected: bool, dmarc_output: DmarcOutput, dkim_output: &[DkimOutput<'_>], + dkim2_output: Option<&Dkim2Output<'_>>, arc_output: &Option>, ) { let dmarc_record = dmarc_output.dmarc_record_cloned().unwrap(); @@ -282,6 +284,9 @@ impl Session { .map(|mf| mf.domain.as_str()) .unwrap_or_else(|| self.data.helo_domain.as_str()), ); + if let Some(dkim2_output) = dkim2_output { + report_record = report_record.with_dkim2_output(dkim2_output); + } if let Some(spf_ehlo) = &self.data.spf_ehlo { report_record = report_record.with_spf_output(spf_ehlo, SPFDomainScope::Helo); } diff --git a/crates/smtp/src/reporting/send.rs b/crates/smtp/src/reporting/send.rs index 3953db29..c0d1349e 100644 --- a/crates/smtp/src/reporting/send.rs +++ b/crates/smtp/src/reporting/send.rs @@ -151,7 +151,7 @@ impl MtaReportSend for Server { Ok(Some(signers)) => { let mut headers = Vec::with_capacity(64); - for signer in signers.as_ref() { + for signer in &signers.dkim1 { match signer.sign(bytes) { Ok(signature) => { signature.write_header(&mut headers); diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index 074d8643..a8cf4c77 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -282,7 +282,7 @@ impl RunScript for Server { Ok(Some(signers)) => { let mut headers = Vec::new(); - for signer in signers.as_ref() { + for signer in &signers.dkim1 { match signer.sign(raw_message) { Ok(signature) => { signature.write_header(&mut headers); diff --git a/crates/spam-filter/src/analysis/dmarc.rs b/crates/spam-filter/src/analysis/dmarc.rs index 230ff27f..9b4903a6 100644 --- a/crates/spam-filter/src/analysis/dmarc.rs +++ b/crates/spam-filter/src/analysis/dmarc.rs @@ -7,7 +7,7 @@ use std::future::Future; use common::Server; -use mail_auth::{DkimResult, DmarcResult, SpfResult, dmarc::Policy}; +use mail_auth::{Dkim2Result, DkimResult, DmarcResult, SpfResult, dmarc::Policy}; use crate::SpamFilterContext; @@ -52,6 +52,18 @@ impl SpamFilterAnalyzeDmarc for Server { }, ); + ctx.result.add_tag( + ctx.input + .dkim2_result + .map_or("DKIM2_NA", |r| match r.result() { + Dkim2Result::Pass => "DKIM2_ALLOW", + Dkim2Result::Fail(_) => "DKIM2_REJECT", + Dkim2Result::PermError(_) => "DKIM2_PERMFAIL", + Dkim2Result::TempError(_) => "DKIM2_TEMPFAIL", + Dkim2Result::None => "DKIM2_NA", + }), + ); + ctx.result .add_tag(ctx.input.arc_result.map_or("ARC_NA", |r| match r.result() { DkimResult::Pass => "ARC_ALLOW", diff --git a/crates/spam-filter/src/lib.rs b/crates/spam-filter/src/lib.rs index a04d4f25..9657c330 100644 --- a/crates/spam-filter/src/lib.rs +++ b/crates/spam-filter/src/lib.rs @@ -11,7 +11,9 @@ pub mod modules; use analysis::ElementLocation; use analysis::url::UrlParts; -use mail_auth::{ArcOutput, DkimOutput, DmarcResult, IprevOutput, SpfOutput, dmarc::Policy}; +use mail_auth::{ + ArcOutput, DkimOutput, DmarcResult, IprevOutput, SpfOutput, dkim2::Dkim2Output, dmarc::Policy, +}; use mail_parser::Message; use modules::html::HtmlToken; use nlp::tokenizers::types::TokenType; @@ -30,6 +32,7 @@ pub struct SpamFilterInput<'x> { pub spf_ehlo_result: Option<&'x SpfOutput>, pub spf_mail_from_result: Option<&'x SpfOutput>, pub dkim_result: &'x [DkimOutput<'x>], + pub dkim2_result: Option<&'x Dkim2Output<'x>>, pub dmarc_result: Option<&'x DmarcResult>, pub dmarc_policy: Option<&'x Policy>, pub iprev_result: Option<&'x IprevOutput>, @@ -147,6 +150,7 @@ impl<'x> SpamFilterInput<'x> { spf_ehlo_result: None, spf_mail_from_result: None, dkim_result: &[], + dkim2_result: None, dmarc_result: None, dmarc_policy: None, iprev_result: None, diff --git a/crates/trc/src/event/conv.rs b/crates/trc/src/event/conv.rs index ddfab62d..db467960 100644 --- a/crates/trc/src/event/conv.rs +++ b/crates/trc/src/event/conv.rs @@ -442,6 +442,36 @@ impl From<&mail_auth::DkimResult> for Error { } } +impl From<&mail_auth::Dkim2Result> for Error { + fn from(value: &mail_auth::Dkim2Result) -> Self { + match value.clone() { + mail_auth::Dkim2Result::Pass => Error::new(EventType::Dkim(DkimEvent::Pass)), + mail_auth::Dkim2Result::Fail(err) => { + Error::new(EventType::Dkim(DkimEvent::Fail)).caused_by(Error::from(err)) + } + mail_auth::Dkim2Result::PermError(err) => { + Error::new(EventType::Dkim(DkimEvent::PermError)).caused_by(Error::from(err)) + } + mail_auth::Dkim2Result::TempError(err) => { + Error::new(EventType::Dkim(DkimEvent::TempError)).caused_by(Error::from(err)) + } + mail_auth::Dkim2Result::None => Error::new(EventType::Dkim(DkimEvent::None)), + } + } +} + +impl From<&mail_auth::dkim2::Dkim2Output<'_>> for Error { + fn from(value: &mail_auth::dkim2::Dkim2Output<'_>) -> Self { + Error::from(value.result()).ctx_opt( + Key::Domain, + value + .chain() + .first() + .map(|link| link.signature.d.to_compact_string()), + ) + } +} + impl From<&mail_auth::DmarcResult> for Error { fn from(value: &mail_auth::DmarcResult) -> Self { match value.clone() { diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index aa4523ae..88b88547 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -6,8 +6,8 @@ // This file is auto-generated. Do not edit directly. -pub const TOTAL_EVENT_COUNT: usize = 630; -pub const TOTAL_METRIC_COUNT: usize = 364; +pub const TOTAL_EVENT_COUNT: usize = 633; +pub const TOTAL_METRIC_COUNT: usize = 367; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EventType { @@ -681,6 +681,9 @@ pub enum SmtpEvent { LoopDetected = 443, DkimPass = 422, DkimFail = 421, + Dkim2Pass = 630, + Dkim2Fail = 631, + Dkim2DsnDiscarded = 632, ArcPass = 410, ArcFail = 409, SpfEhloPass = 474, @@ -1182,6 +1185,9 @@ pub enum MetricType { SmtpLoopDetected = 255, SmtpDkimPass = 256, SmtpDkimFail = 257, + SmtpDkim2Pass = 364, + SmtpDkim2Fail = 365, + SmtpDkim2DsnDiscarded = 366, SmtpArcPass = 258, SmtpArcFail = 259, SmtpSpfEhloPass = 260, diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index e3cec6d6..666812b6 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -466,6 +466,9 @@ impl EventType { b"smtp.loop-detected" => EventType::Smtp(SmtpEvent::LoopDetected), b"smtp.dkim-pass" => EventType::Smtp(SmtpEvent::DkimPass), b"smtp.dkim-fail" => EventType::Smtp(SmtpEvent::DkimFail), + b"smtp.dkim2-pass" => EventType::Smtp(SmtpEvent::Dkim2Pass), + b"smtp.dkim2-fail" => EventType::Smtp(SmtpEvent::Dkim2Fail), + b"smtp.dkim2-dsn-discarded" => EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded), b"smtp.arc-pass" => EventType::Smtp(SmtpEvent::ArcPass), b"smtp.arc-fail" => EventType::Smtp(SmtpEvent::ArcFail), b"smtp.spf-ehlo-pass" => EventType::Smtp(SmtpEvent::SpfEhloPass), @@ -1218,6 +1221,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected) => "smtp.loop-detected", EventType::Smtp(SmtpEvent::DkimPass) => "smtp.dkim-pass", EventType::Smtp(SmtpEvent::DkimFail) => "smtp.dkim-fail", + EventType::Smtp(SmtpEvent::Dkim2Pass) => "smtp.dkim2-pass", + EventType::Smtp(SmtpEvent::Dkim2Fail) => "smtp.dkim2-fail", + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded) => "smtp.dkim2-dsn-discarded", EventType::Smtp(SmtpEvent::ArcPass) => "smtp.arc-pass", EventType::Smtp(SmtpEvent::ArcFail) => "smtp.arc-fail", EventType::Smtp(SmtpEvent::SpfEhloPass) => "smtp.spf-ehlo-pass", @@ -1873,6 +1879,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected) => 443, EventType::Smtp(SmtpEvent::DkimPass) => 422, EventType::Smtp(SmtpEvent::DkimFail) => 421, + EventType::Smtp(SmtpEvent::Dkim2Pass) => 630, + EventType::Smtp(SmtpEvent::Dkim2Fail) => 631, + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded) => 632, EventType::Smtp(SmtpEvent::ArcPass) => 410, EventType::Smtp(SmtpEvent::ArcFail) => 409, EventType::Smtp(SmtpEvent::SpfEhloPass) => 474, @@ -2546,6 +2555,9 @@ impl EventType { 443 => Some(EventType::Smtp(SmtpEvent::LoopDetected)), 422 => Some(EventType::Smtp(SmtpEvent::DkimPass)), 421 => Some(EventType::Smtp(SmtpEvent::DkimFail)), + 630 => Some(EventType::Smtp(SmtpEvent::Dkim2Pass)), + 631 => Some(EventType::Smtp(SmtpEvent::Dkim2Fail)), + 632 => Some(EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded)), 410 => Some(EventType::Smtp(SmtpEvent::ArcPass)), 409 => Some(EventType::Smtp(SmtpEvent::ArcFail)), 474 => Some(EventType::Smtp(SmtpEvent::SpfEhloPass)), @@ -2940,6 +2952,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected) => Level::Info, EventType::Smtp(SmtpEvent::DkimPass) => Level::Info, EventType::Smtp(SmtpEvent::DkimFail) => Level::Info, + EventType::Smtp(SmtpEvent::Dkim2Pass) => Level::Info, + EventType::Smtp(SmtpEvent::Dkim2Fail) => Level::Info, + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded) => Level::Info, EventType::Smtp(SmtpEvent::ArcPass) => Level::Info, EventType::Smtp(SmtpEvent::ArcFail) => Level::Info, EventType::Smtp(SmtpEvent::SpfEhloPass) => Level::Info, @@ -3659,6 +3674,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected) => "Mail loop detected", EventType::Smtp(SmtpEvent::DkimPass) => "DKIM verification passed", EventType::Smtp(SmtpEvent::DkimFail) => "DKIM verification failed", + EventType::Smtp(SmtpEvent::Dkim2Pass) => "DKIM2 verification passed", + EventType::Smtp(SmtpEvent::Dkim2Fail) => "DKIM2 verification failed", + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded) => "DKIM2 DSN discarded", EventType::Smtp(SmtpEvent::ArcPass) => "ARC verification passed", EventType::Smtp(SmtpEvent::ArcFail) => "ARC verification failed", EventType::Smtp(SmtpEvent::SpfEhloPass) => "SPF EHLO check passed", @@ -4024,6 +4042,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected) => "SMTP error", EventType::Smtp(SmtpEvent::DkimPass) => "SMTP error", EventType::Smtp(SmtpEvent::DkimFail) => "SMTP error", + EventType::Smtp(SmtpEvent::Dkim2Pass) => "SMTP error", + EventType::Smtp(SmtpEvent::Dkim2Fail) => "SMTP error", + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded) => "SMTP error", EventType::Smtp(SmtpEvent::ArcPass) => "SMTP error", EventType::Smtp(SmtpEvent::ArcFail) => "SMTP error", EventType::Smtp(SmtpEvent::SpfEhloPass) => "SMTP error", @@ -4589,6 +4610,9 @@ impl EventType { EventType::Smtp(SmtpEvent::LoopDetected), EventType::Smtp(SmtpEvent::DkimPass), EventType::Smtp(SmtpEvent::DkimFail), + EventType::Smtp(SmtpEvent::Dkim2Pass), + EventType::Smtp(SmtpEvent::Dkim2Fail), + EventType::Smtp(SmtpEvent::Dkim2DsnDiscarded), EventType::Smtp(SmtpEvent::ArcPass), EventType::Smtp(SmtpEvent::ArcFail), EventType::Smtp(SmtpEvent::SpfEhloPass), @@ -5054,6 +5078,9 @@ impl MetricType { b"smtp.loop-detected" => MetricType::SmtpLoopDetected, b"smtp.dkim-pass" => MetricType::SmtpDkimPass, b"smtp.dkim-fail" => MetricType::SmtpDkimFail, + b"smtp.dkim2-pass" => MetricType::SmtpDkim2Pass, + b"smtp.dkim2-fail" => MetricType::SmtpDkim2Fail, + b"smtp.dkim2-dsn-discarded" => MetricType::SmtpDkim2DsnDiscarded, b"smtp.arc-pass" => MetricType::SmtpArcPass, b"smtp.arc-fail" => MetricType::SmtpArcFail, b"smtp.spf-ehlo-pass" => MetricType::SmtpSpfEhloPass, @@ -5434,6 +5461,9 @@ impl MetricType { MetricType::SmtpLoopDetected => "smtp.loop-detected", MetricType::SmtpDkimPass => "smtp.dkim-pass", MetricType::SmtpDkimFail => "smtp.dkim-fail", + MetricType::SmtpDkim2Pass => "smtp.dkim2-pass", + MetricType::SmtpDkim2Fail => "smtp.dkim2-fail", + MetricType::SmtpDkim2DsnDiscarded => "smtp.dkim2-dsn-discarded", MetricType::SmtpArcPass => "smtp.arc-pass", MetricType::SmtpArcFail => "smtp.arc-fail", MetricType::SmtpSpfEhloPass => "smtp.spf-ehlo-pass", @@ -5805,6 +5835,9 @@ impl MetricType { MetricType::SmtpLoopDetected => 255, MetricType::SmtpDkimPass => 256, MetricType::SmtpDkimFail => 257, + MetricType::SmtpDkim2Pass => 364, + MetricType::SmtpDkim2Fail => 365, + MetricType::SmtpDkim2DsnDiscarded => 366, MetricType::SmtpArcPass => 258, MetricType::SmtpArcFail => 259, MetricType::SmtpSpfEhloPass => 260, @@ -6174,6 +6207,9 @@ impl MetricType { 255 => Some(MetricType::SmtpLoopDetected), 256 => Some(MetricType::SmtpDkimPass), 257 => Some(MetricType::SmtpDkimFail), + 364 => Some(MetricType::SmtpDkim2Pass), + 365 => Some(MetricType::SmtpDkim2Fail), + 366 => Some(MetricType::SmtpDkim2DsnDiscarded), 258 => Some(MetricType::SmtpArcPass), 259 => Some(MetricType::SmtpArcFail), 260 => Some(MetricType::SmtpSpfEhloPass), @@ -6522,6 +6558,9 @@ impl MetricType { MetricType::SmtpLoopDetected => 443, MetricType::SmtpDkimPass => 422, MetricType::SmtpDkimFail => 421, + MetricType::SmtpDkim2Pass => 630, + MetricType::SmtpDkim2Fail => 631, + MetricType::SmtpDkim2DsnDiscarded => 632, MetricType::SmtpArcPass => 410, MetricType::SmtpArcFail => 409, MetricType::SmtpSpfEhloPass => 474, @@ -6899,6 +6938,9 @@ impl MetricType { MetricType::SmtpLoopDetected => "Mail loop detected", MetricType::SmtpDkimPass => "DKIM verification passed", MetricType::SmtpDkimFail => "DKIM verification failed", + MetricType::SmtpDkim2Pass => "DKIM2 verification passed", + MetricType::SmtpDkim2Fail => "DKIM2 verification failed", + MetricType::SmtpDkim2DsnDiscarded => "DKIM2 DSN discarded", MetricType::SmtpArcPass => "ARC verification passed", MetricType::SmtpArcFail => "ARC verification failed", MetricType::SmtpSpfEhloPass => "SPF EHLO check passed", @@ -7256,6 +7298,9 @@ impl MetricType { | MetricType::SmtpLoopDetected | MetricType::SmtpDkimPass | MetricType::SmtpDkimFail + | MetricType::SmtpDkim2Pass + | MetricType::SmtpDkim2Fail + | MetricType::SmtpDkim2DsnDiscarded | MetricType::SmtpArcPass | MetricType::SmtpArcFail | MetricType::SmtpSpfEhloPass @@ -7637,6 +7682,9 @@ impl MetricType { MetricType::SmtpLoopDetected, MetricType::SmtpDkimPass, MetricType::SmtpDkimFail, + MetricType::SmtpDkim2Pass, + MetricType::SmtpDkim2Fail, + MetricType::SmtpDkim2DsnDiscarded, MetricType::SmtpArcPass, MetricType::SmtpArcFail, MetricType::SmtpSpfEhloPass, diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index d46002f8..db069c26 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 017aae52..5b1bcc22 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -FLxK3azkzVxELInWViuc-_dg8JEbSF7YkWZfH-PjzeA \ No newline at end of file +hFFGU64dDXCQt2y4dqKbd_2qzMdcWCa1eKu2PT1zzrM \ No newline at end of file diff --git a/tests/resources/smtp/antispam/combined.test b/tests/resources/smtp/antispam/combined.test index 4580d5d8..ee411e8a 100644 --- a/tests/resources/smtp/antispam/combined.test +++ b/tests/resources/smtp/antispam/combined.test @@ -6,7 +6,7 @@ spf.result none spf_ehlo.result none dmarc.result none remote_ip 195.210.29.48 -expect_header X-Spam-Result: ARC_NA (0.00), DKIM_NA (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_DATA_URI (0.00), HAS_LINK_TO_LARGE_IMG (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROM (0.00), RCPT_COUNT_ONE (0.00), SPF_NA (0.00), SUBJECT_ENDS_EXCLAIM (0.00), TO_DN_NONE (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), MIME_HTML_ONLY (0.20), HELO_NORES_A_OR_MX (0.30), AUTH_NA (1.00), DATE_IN_PAST (1.00), DMARC_NA (1.00), MID_RHS_MATCH_FROM (1.00), FROMHOST_NORES_A_OR_MX (1.50), HTML_SHORT_LINK_IMG_1 (2.00), RDNS_NONE (2.00), PYZOR (3.50) +expect_header X-Spam-Result: ARC_NA (0.00), DKIM2_NA (0.00), DKIM_NA (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_DATA_URI (0.00), HAS_LINK_TO_LARGE_IMG (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROM (0.00), RCPT_COUNT_ONE (0.00), SPF_NA (0.00), SUBJECT_ENDS_EXCLAIM (0.00), TO_DN_NONE (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), MIME_HTML_ONLY (0.20), HELO_NORES_A_OR_MX (0.30), AUTH_NA (1.00), DATE_IN_PAST (1.00), DMARC_NA (1.00), MID_RHS_MATCH_FROM (1.00), FROMHOST_NORES_A_OR_MX (1.50), HTML_SHORT_LINK_IMG_1 (2.00), RDNS_NONE (2.00), PYZOR (3.50) expect_header X-Spam-Score: spam, score=13.70 From: Client Services @@ -49,7 +49,7 @@ dkim.domains tenthrevolution.com dmarc.result pass remote_ip 185.58.86.181 tls.version TLSv1.3 -expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), MIME_GOOD (-0.10), ARC_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_ATTACHMENT (0.00), HTML_SHORT_2 (0.00), RCPT_COUNT_ONE (0.00), RCVD_COUNT_THREE (0.00), TO_DN_EQ_ADDR_ALL (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), SUBJECT_ENDS_SPACES (0.50), URI_COUNT_ODD (0.50), DATE_IN_PAST (1.00), FORGED_RCVD_TRAIL (1.00), FROMHOST_NORES_A_OR_MX (1.50) +expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), MIME_GOOD (-0.10), ARC_NA (0.00), DKIM2_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_ATTACHMENT (0.00), HTML_SHORT_2 (0.00), RCPT_COUNT_ONE (0.00), RCVD_COUNT_THREE (0.00), TO_DN_EQ_ADDR_ALL (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), SUBJECT_ENDS_SPACES (0.50), URI_COUNT_ODD (0.50), DATE_IN_PAST (1.00), FORGED_RCVD_TRAIL (1.00), FROMHOST_NORES_A_OR_MX (1.50) expect_header X-Spam-Score: ham, score=3.90 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tenthrevolution.com; @@ -574,7 +574,7 @@ dmarc.result fail dmarc.policy reject remote_ip 51.89.165.39 tls.version TLS1_2 -expect_header X-Spam-Result: DKIM_ALLOW (-0.20), HAS_LIST_UNSUB (-0.01), ARC_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_EXTERNAL_IMG (0.00), HAS_LINK_TO_LARGE_IMG (0.00), HAS_REPLYTO (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROM (0.00), RCPT_COUNT_ONE (0.00), REPLYTO_ADDR_EQ_FROM (0.00), REPLYTO_EQ_FROM (0.00), SPF_SOFTFAIL (0.00), TO_DN_NONE (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), DATE_IN_PAST (1.00), MID_RHS_MATCH_FROM (1.00), PARTS_DIFFER (1.00), FROMHOST_NORES_A_OR_MX (1.50), HTML_SHORT_LINK_IMG_1 (2.00), RDNS_NONE (2.00), VIOLATED_DIRECT_SPF (3.50), DMARC_POLICY_REJECT (4.00) +expect_header X-Spam-Result: DKIM_ALLOW (-0.20), HAS_LIST_UNSUB (-0.01), ARC_NA (0.00), DKIM2_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_EXTERNAL_IMG (0.00), HAS_LINK_TO_LARGE_IMG (0.00), HAS_REPLYTO (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROM (0.00), RCPT_COUNT_ONE (0.00), REPLYTO_ADDR_EQ_FROM (0.00), REPLYTO_EQ_FROM (0.00), SPF_SOFTFAIL (0.00), TO_DN_NONE (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), DATE_IN_PAST (1.00), MID_RHS_MATCH_FROM (1.00), PARTS_DIFFER (1.00), FROMHOST_NORES_A_OR_MX (1.50), HTML_SHORT_LINK_IMG_1 (2.00), RDNS_NONE (2.00), VIOLATED_DIRECT_SPF (3.50), DMARC_POLICY_REJECT (4.00) expect_header X-Spam-Score: spam, score=16.29 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=sectionalism; d=grupokonecta.net; @@ -791,7 +791,7 @@ dmarc.result pass dmarc.policy reject remote_ip 52.103.64.5 tls.version TLS1_2 -expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), ARC_NA (0.00), ARC_SIGNED (0.00), DKIM_SIGNED (0.00), FREEMAIL_FROM (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_SEO_WORD (0.00), HAS_X_PRIO_ONE (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROMTLD (0.00), MID_RHS_MATCH_FROMTLD (0.00), RCPT_COUNT_ONE (0.00), RCPT_IN_BODY (0.00), RCVD_COUNT_TWO (0.00), TO_DN_EQ_ADDR_ALL (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), DATE_IN_PAST (1.00), FROMHOST_NORES_A_OR_MX (1.50), SEO_SPAM (5.00) +expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), ARC_NA (0.00), ARC_SIGNED (0.00), DKIM2_NA (0.00), DKIM_SIGNED (0.00), FREEMAIL_FROM (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_SEO_WORD (0.00), HAS_X_PRIO_ONE (0.00), HTML_SHORT_1 (0.00), MID_RHS_MATCH_ENV_FROMTLD (0.00), MID_RHS_MATCH_FROMTLD (0.00), RCPT_COUNT_ONE (0.00), RCPT_IN_BODY (0.00), RCVD_COUNT_TWO (0.00), TO_DN_EQ_ADDR_ALL (0.00), TO_MATCH_ENVRCPT_ALL (0.00), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), DATE_IN_PAST (1.00), FROMHOST_NORES_A_OR_MX (1.50), SEO_SPAM (5.00) expect_header X-Spam-Score: spam, score=7.00 Return-Path: @@ -1021,7 +1021,7 @@ dmarc.result pass dmarc.policy reject remote_ip 173.224.123.255 tls.version TLS1_2 -expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), ARC_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_EXTERNAL_IMG (0.00), HAS_REPLYTO (0.00), HAS_X_PRIO_THREE (0.00), HTML_SHORT_1 (0.00), RCPT_COUNT_ONE (0.00), REPLYTO_DN_EQ_FROM_DN (0.00), REPLYTO_DOM_EQ_FROM_DOM (0.00), TO_DN_ALL (0.00), TO_EQ_FROM (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), MID_RHS_NOT_FQDN (0.50), UNPARSABLE_URL (0.50), DATE_IN_PAST (1.00), FROMHOST_NORES_A_OR_MX (1.50), DIRECT_TO_MX (2.00), FORGED_RECIPIENTS (2.00), SUBJ_ALL_CAPS (3.00) +expect_header X-Spam-Result: DMARC_POLICY_ALLOW (-0.50), DKIM_ALLOW (-0.20), SPF_ALLOW (-0.20), ARC_NA (0.00), DKIM2_NA (0.00), DKIM_SIGNED (0.00), FROM_EQ_ENV_FROM (0.00), FROM_HAS_DN (0.00), HAS_EXTERNAL_IMG (0.00), HAS_REPLYTO (0.00), HAS_X_PRIO_THREE (0.00), HTML_SHORT_1 (0.00), RCPT_COUNT_ONE (0.00), REPLYTO_DN_EQ_FROM_DN (0.00), REPLYTO_DOM_EQ_FROM_DOM (0.00), TO_DN_ALL (0.00), TO_EQ_FROM (0.00), RCVD_COUNT_ZERO (0.10), RCVD_NO_TLS_LAST (0.10), HELO_NORES_A_OR_MX (0.30), MID_RHS_NOT_FQDN (0.50), UNPARSABLE_URL (0.50), DATE_IN_PAST (1.00), FROMHOST_NORES_A_OR_MX (1.50), DIRECT_TO_MX (2.00), FORGED_RECIPIENTS (2.00), SUBJ_ALL_CAPS (3.00) expect_header X-Spam-Score: spam, score=10.10 Return-Path: diff --git a/tests/resources/smtp/antispam/dmarc.test b/tests/resources/smtp/antispam/dmarc.test index 9e3a77e3..feaa1217 100644 --- a/tests/resources/smtp/antispam/dmarc.test +++ b/tests/resources/smtp/antispam/dmarc.test @@ -1,4 +1,4 @@ -expect DMARC_NA SPF_NA DKIM_NA ARC_NA AUTH_NA +expect DMARC_NA SPF_NA DKIM_NA ARC_NA AUTH_NA DKIM2_NA Subject: test @@ -9,7 +9,7 @@ spf.result pass dkim.result pass arc.result pass dmarc.result pass -expect DKIM_SIGNED ARC_SIGNED DKIM_ALLOW SPF_ALLOW ARC_ALLOW DMARC_POLICY_ALLOW +expect DKIM_SIGNED ARC_SIGNED DKIM_ALLOW SPF_ALLOW ARC_ALLOW DMARC_POLICY_ALLOW DKIM2_NA DKIM-Signature: abc ARC-Seal: xyz @@ -23,7 +23,7 @@ dkim.result fail arc.result fail dmarc.result fail dmarc.policy quarantine -expect SPF_FAIL ARC_REJECT DKIM_REJECT DMARC_POLICY_QUARANTINE +expect SPF_FAIL ARC_REJECT DKIM_REJECT DMARC_POLICY_QUARANTINE DKIM2_NA Subject: test @@ -35,7 +35,7 @@ dkim.result temperror arc.result permerror dmarc.result fail dmarc.policy reject -expect DKIM_TEMPFAIL SPF_NEUTRAL ARC_INVALID DMARC_POLICY_REJECT +expect DKIM_TEMPFAIL SPF_NEUTRAL ARC_INVALID DMARC_POLICY_REJECT DKIM2_NA Subject: test @@ -46,7 +46,7 @@ spf.result softfail dkim.result permerror arc.result temperror dmarc.result permerror -expect ARC_DNSFAIL DMARC_BAD_POLICY DKIM_PERMFAIL SPF_SOFTFAIL +expect ARC_DNSFAIL DMARC_BAD_POLICY DKIM_PERMFAIL SPF_SOFTFAIL DKIM2_NA Subject: test @@ -55,7 +55,7 @@ Test dkim.result pass dkim.domains spf-dkim-allow.org spf.result pass -expect DKIM_ALLOW SPF_ALLOW ARC_NA DMARC_NA +expect DKIM_ALLOW SPF_ALLOW ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -65,7 +65,7 @@ Test dkim.result pass spf.result pass arc.result pass -expect DKIM_ALLOW SPF_ALLOW ARC_ALLOW DMARC_NA +expect DKIM_ALLOW SPF_ALLOW ARC_ALLOW DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -74,7 +74,7 @@ Test spf.result pass dkim.result fail -expect DKIM_REJECT SPF_ALLOW ARC_NA DMARC_NA +expect DKIM_REJECT SPF_ALLOW ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -83,7 +83,7 @@ Test spf.result pass dkim.result temperror -expect DKIM_TEMPFAIL SPF_ALLOW ARC_NA DMARC_NA +expect DKIM_TEMPFAIL SPF_ALLOW ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -93,7 +93,7 @@ Test dkim.result pass dkim.domains spf-dkim-allow.org spf.result fail -expect DKIM_ALLOW SPF_FAIL ARC_NA DMARC_NA +expect DKIM_ALLOW SPF_FAIL ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -103,7 +103,7 @@ Test dkim.result pass dkim.domains spf-dkim-allow.org spf.result temperror -expect DKIM_ALLOW SPF_DNSFAIL ARC_NA DMARC_NA +expect DKIM_ALLOW SPF_DNSFAIL ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -112,7 +112,7 @@ Test dkim.result fail spf.result fail -expect DKIM_REJECT SPF_FAIL ARC_NA DMARC_NA +expect DKIM_REJECT SPF_FAIL ARC_NA DMARC_NA DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -121,7 +121,7 @@ Test dkim.result temperror spf.result temperror -expect DKIM_TEMPFAIL SPF_DNSFAIL ARC_NA DMARC_NA AUTH_NA_OR_FAIL +expect DKIM_TEMPFAIL SPF_DNSFAIL ARC_NA DMARC_NA AUTH_NA_OR_FAIL DKIM2_NA From: user@spf-dkim-allow.org Subject: test @@ -133,7 +133,7 @@ dkim.result pass arc.result pass dmarc.result pass envelope_from hello@stalw.art -expect TRUSTED_DOMAIN DMARC_POLICY_ALLOW DKIM_ALLOW SPF_ALLOW ARC_ALLOW +expect TRUSTED_DOMAIN DMARC_POLICY_ALLOW DKIM_ALLOW SPF_ALLOW ARC_ALLOW DKIM2_NA From: diff --git a/tests/src/automation/dkim.rs b/tests/src/automation/dkim.rs index bbdd2fa4..f80f5c6d 100644 --- a/tests/src/automation/dkim.rs +++ b/tests/src/automation/dkim.rs @@ -6,7 +6,7 @@ use crate::utils::{account::Account, server::TestServer}; use ahash::AHashSet; -use common::{config::smtp::auth::DkimSigner, network::dns::update::DNS_RECORDS}; +use common::{config::smtp::auth::Dkim1Signer, network::dns::update::DNS_RECORDS}; use dns_update::{DnsRecord, NamedDnsRecord}; use registry::{ schema::{ @@ -316,10 +316,11 @@ impl TestServer { .await .unwrap() .unwrap_or_else(|| panic!("No signatures found: {:?}", selectors)) + .dkim1 .iter() .map(|s| match s { - DkimSigner::RsaSha256(s) => s.template.s.as_str(), - DkimSigner::Ed25519Sha256(s) => s.template.s.as_str(), + Dkim1Signer::RsaSha256(s) => s.template.s.as_str(), + Dkim1Signer::Ed25519Sha256(s) => s.template.s.as_str(), }) .collect::>(), selectors.iter().copied().collect::>() diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index a099eb75..97a03d98 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -520,6 +520,7 @@ async fn antispam() { .spam_classify( &parsed_message, &dkim_domains, + None, arc_result.as_ref(), dmarc_result.as_ref(), dmarc_policy.as_ref(), @@ -549,6 +550,7 @@ async fn antispam() { let mut spam_input = session.build_spam_input( &parsed_message, &dkim_domains, + None, arc_result.as_ref(), dmarc_result.as_ref(), dmarc_policy.as_ref(),