DKIM2 tests
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user