DKIM2 tests

This commit is contained in:
Maurus Decimus
2026-07-03 20:07:37 +02:00
parent c5796b4e7a
commit 94b33187a0
16 changed files with 1124 additions and 148 deletions

View File

@@ -21,7 +21,7 @@ email = { path = "../email" }
registry = { path = "../registry" }
spam-filter = { path = "../spam-filter" }
trc = { path = "../trc" }
mail-auth = { path = "/Users/me/code/mail-auth", features = ["rkyv"] }
mail-auth = { version = "0.11", features = ["rkyv"] }
mail-parser = { version = "0.11", features = ["full_encoding"] }
mail-builder = { version = "0.4" }
smtp-proto = { version = "0.2", features = ["rkyv", "serde"] }

View File

@@ -85,7 +85,7 @@ impl DkimSign for Server {
});
let instance = MessageInstance::from_message(&modified, original.as_ref());
if let Some(instance) = &instance {
instance.write(&mut headers);
instance.write_header(&mut headers);
}
// Obtain disclosed and undisclosed recipients
@@ -104,11 +104,11 @@ impl DkimSign for Server {
Ok(signature) => {
if envelopes.undisclosed_recipients.is_empty() {
// Happy path: no undisclosed recipients, serialize signature straight to blob
signature.write(&mut headers);
signature.write_header(&mut headers);
} else {
// Undisclosed recipients present, serialize signature to metadata
let mut header = Vec::with_capacity(64);
signature.write(&mut header);
signature.write_header(&mut header);
params.metadata.push(Metadata::Headers {
value: header.into_boxed_slice(),
id: u64::MAX,
@@ -135,7 +135,7 @@ impl DkimSign for Server {
Ok(signature) => {
// Serialize signature to metadata
let mut header = Vec::with_capacity(64);
signature.write(&mut header);
signature.write_header(&mut header);
params.metadata.push(Metadata::Headers {
value: header.into_boxed_slice(),
id: pos as u64,
@@ -251,3 +251,168 @@ impl MessageWrapper {
fn sanitize_or_lower(rcpt: &str) -> String {
sanitize_email(rcpt).unwrap_or_else(|| rcpt.to_lowercase())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::queue::{Message, QueueId, Recipient};
use common::config::smtp::queue::QueueName;
use std::net::{IpAddr, Ipv4Addr};
fn wrapper(recipients: &[&str]) -> MessageWrapper {
MessageWrapper {
queue_id: 0 as QueueId,
queue_name: QueueName::default(),
is_multi_queue: false,
span_id: 0,
message: Message {
created: 0,
blob_hash: Default::default(),
return_path: "sender@example.com".into(),
recipients: recipients.iter().map(Recipient::new).collect(),
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
received_via_port: 0,
flags: 0,
env_id: None,
priority: 0,
size: 0,
metadata: Default::default(),
},
}
}
fn split(headers: &str, recipients: &[&str]) -> (Vec<String>, Vec<String>) {
let raw = format!("{headers}\r\nSubject: test\r\n\r\nbody\r\n");
let auth = AuthenticatedMessage::parse(raw.as_bytes()).expect("parse message");
let message = wrapper(recipients);
let envelopes = message.undisclosed_recipients(&auth);
let mut disclosed = envelopes
.disclosed_recipients
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let mut undisclosed = envelopes
.undisclosed_recipients
.iter()
.map(|(_, s)| s.to_string())
.collect::<Vec<_>>();
disclosed.sort();
undisclosed.sort();
(disclosed, undisclosed)
}
#[test]
fn single_recipient_is_always_disclosed() {
let (disclosed, undisclosed) = split("To: someone-else@example.com", &["bcc@example.org"]);
assert_eq!(disclosed, vec!["bcc@example.org".to_string()]);
assert!(undisclosed.is_empty());
}
#[test]
fn all_recipients_disclosed() {
let (disclosed, undisclosed) = split(
"To: alice@example.com, bob@example.com\r\nCc: carol@example.com",
&["alice@example.com", "bob@example.com", "carol@example.com"],
);
assert_eq!(
disclosed,
vec![
"alice@example.com".to_string(),
"bob@example.com".to_string(),
"carol@example.com".to_string(),
]
);
assert!(undisclosed.is_empty());
}
#[test]
fn mixed_disclosed_and_undisclosed() {
let (disclosed, undisclosed) = split(
"To: alice@example.com\r\nCc: bob@example.com",
&[
"alice@example.com",
"bob@example.com",
"eve@secret.example.org",
"mallory@secret.example.org",
],
);
assert_eq!(
disclosed,
vec![
"alice@example.com".to_string(),
"bob@example.com".to_string()
]
);
assert_eq!(
undisclosed,
vec![
"eve@secret.example.org".to_string(),
"mallory@secret.example.org".to_string(),
]
);
}
#[test]
fn no_to_or_cc_header_all_undisclosed() {
let (disclosed, undisclosed) = split(
"From: sender@example.com",
&["eve@example.org", "mallory@example.org"],
);
assert!(disclosed.is_empty());
assert_eq!(
undisclosed,
vec![
"eve@example.org".to_string(),
"mallory@example.org".to_string()
]
);
}
#[test]
fn group_addresses_are_disclosed() {
let (disclosed, undisclosed) = split(
"To: Team:alice@example.com,bob@example.com;",
&["alice@example.com", "bob@example.com", "eve@example.org"],
);
assert_eq!(
disclosed,
vec![
"alice@example.com".to_string(),
"bob@example.com".to_string()
]
);
assert_eq!(undisclosed, vec!["eve@example.org".to_string()]);
}
#[test]
fn header_address_casing_is_ignored() {
let (disclosed, undisclosed) = split(
"To: Alice@Example.COM, BOB@EXAMPLE.com",
&["alice@example.com", "bob@example.com", "eve@example.org"],
);
assert_eq!(
disclosed,
vec![
"alice@example.com".to_string(),
"bob@example.com".to_string()
]
);
assert_eq!(undisclosed, vec!["eve@example.org".to_string()]);
}
#[test]
fn display_names_and_brackets_are_ignored() {
let (disclosed, undisclosed) = split(
"To: \"Alice Doe\" <alice@example.com>\r\nCc: Bob <bob@example.com>",
&["alice@example.com", "bob@example.com", "eve@example.org"],
);
assert_eq!(
disclosed,
vec![
"alice@example.com".to_string(),
"bob@example.com".to_string()
]
);
assert_eq!(undisclosed, vec!["eve@example.org".to_string()]);
}
}

View File

@@ -50,7 +50,7 @@ pub const SMFIC_UNKNOWN: u8 = b'U'; /* Any unknown command */
impl Command<'_> {
fn build(command: u8, len: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(len as usize + 1 + std::mem::size_of::<u32>());
buf.extend_from_slice((len + 1).to_be_bytes().as_ref());
buf.extend_from_slice((len + 1).to_be_bytes().as_slice());
buf.push(command);
buf
}
@@ -95,7 +95,7 @@ impl Command<'_> {
buf.extend(hostname);
buf.push(0x00);
buf.push(family);
buf.extend(port.to_be_bytes().as_ref());
buf.extend(port.to_be_bytes().as_slice());
buf.extend(address.as_bytes());
buf.push(0x00);
buf
@@ -168,9 +168,9 @@ impl Command<'_> {
}
Command::OptionNegotiation(opt) => {
let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::<u32>() as u32);
buf.extend(opt.version.to_be_bytes().as_ref());
buf.extend(opt.actions.to_be_bytes().as_ref());
buf.extend(opt.protocol.to_be_bytes().as_ref());
buf.extend(opt.version.to_be_bytes().as_slice());
buf.extend(opt.actions.to_be_bytes().as_slice());
buf.extend(opt.protocol.to_be_bytes().as_slice());
buf
}
Command::Quit => Command::build(SMFIC_QUIT, 0),
@@ -417,7 +417,7 @@ impl Response {
+ std::mem::size_of::<u32>() as u32
+ 2,
);
buf.extend(index.to_be_bytes().as_ref());
buf.extend(index.to_be_bytes().as_slice());
buf.extend(name.as_bytes());
buf.push(0x00);
buf.extend(value.as_bytes());
@@ -432,7 +432,7 @@ impl Response {
+ std::mem::size_of::<u32>() as u32
+ 2,
);
buf.extend(index.to_be_bytes().as_ref());
buf.extend(index.to_be_bytes().as_slice());
buf.extend(name.as_bytes());
buf.push(0x00);
buf.extend(value.as_bytes());
@@ -451,9 +451,9 @@ impl Response {
Response::SetSymbols => Command::build(SMFIR_SETSYMLIST, 0),
Response::OptionNegotiation(opt) => {
let mut buf = Command::build(SMFIC_OPTNEG, 3 * std::mem::size_of::<u32>() as u32);
buf.extend(opt.version.to_be_bytes().as_ref());
buf.extend(opt.actions.to_be_bytes().as_ref());
buf.extend(opt.protocol.to_be_bytes().as_ref());
buf.extend(opt.version.to_be_bytes().as_slice());
buf.extend(opt.actions.to_be_bytes().as_slice());
buf.extend(opt.protocol.to_be_bytes().as_slice());
buf
}
}