use super::{
ChainBinding, Dkim2Result, Recipe, Signature, Verification, sign::Envelope,
verify::relaxed_domain_match,
};
use crate::dns::DnsCache;
use crate::headers::HeaderIterator;
use crate::{
AuthenticatedMessage, MessageAuthenticator, Parameters, ResolverCache, TxtRecord,
dkim2::sign::now, dns::NoCache,
};
use mail_parser::MessageParser;
use std::marker::PhantomData;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dkim2Dsn<'x, R = AuthenticatedMessage<'x>, S = AuthenticatedMessage<'x>>
where
R: AsRef<AuthenticatedMessage<'x>>,
S: AsRef<AuthenticatedMessage<'x>>,
{
pub dsn: R,
pub returned: S,
pub returned_full: bool,
_marker: PhantomData<&'x ()>,
}
impl<'x, R, S> Dkim2Dsn<'x, R, S>
where
R: AsRef<AuthenticatedMessage<'x>>,
S: AsRef<AuthenticatedMessage<'x>>,
{
pub fn new(dsn: R, returned: S, returned_full: bool) -> Self {
Dkim2Dsn {
dsn,
returned,
returned_full,
_marker: PhantomData,
}
}
}
impl<'x> Dkim2Dsn<'x> {
pub fn parse(raw_message: &'x [u8]) -> Result<Dkim2Dsn<'x>, Dkim2DsnFailure> {
let message = MessageParser::new()
.parse(raw_message)
.ok_or(Dkim2DsnFailure::DsnUnparseable)?;
let root = message.root_part();
if !root.is_multipart() {
return Err(Dkim2DsnFailure::DsnUnparseable);
}
let mut returned = None;
for part in root.children() {
let slice = raw_message
.get(part.offset_body() as usize..part.offset_end() as usize)
.ok_or(Dkim2DsnFailure::DsnUnparseable)?;
if part.is_content_type("message", "rfc822") {
returned = Some((slice, true));
} else if part.is_content_type("text", "rfc822-headers") {
returned = Some((slice, false));
}
}
let (returned_slice, returned_full) =
returned.ok_or(Dkim2DsnFailure::ReturnedUnparseable)?;
Ok(Dkim2Dsn {
dsn: AuthenticatedMessage::parse(raw_message).ok_or(Dkim2DsnFailure::DsnUnparseable)?,
returned: AuthenticatedMessage::parse(returned_slice)
.ok_or(Dkim2DsnFailure::ReturnedUnparseable)?,
returned_full,
_marker: PhantomData,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dkim2DsnOutput {
pub dsn: Dkim2Result,
pub returned: Dkim2Result,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dkim2DsnFailure {
DsnUnparseable,
ReturnedUnparseable,
DsnNotSigned,
DsnChainFailed,
ReturnedNotSigned,
ReturnedChainFailed,
NotAligned,
TempError,
}
impl std::fmt::Display for Dkim2DsnFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Dkim2DsnFailure::DsnUnparseable => "DSN could not be parsed",
Dkim2DsnFailure::ReturnedUnparseable => "Returned message could not be parsed",
Dkim2DsnFailure::DsnNotSigned => "DSN is not DKIM2 signed",
Dkim2DsnFailure::DsnChainFailed => "DSN signature chain failed",
Dkim2DsnFailure::ReturnedNotSigned => "Returned message is not DKIM2 signed",
Dkim2DsnFailure::ReturnedChainFailed => "Returned message signature chain failed",
Dkim2DsnFailure::NotAligned => {
"DSN signer is not aligned with the returned message recipient"
}
Dkim2DsnFailure::TempError => {
"DKIM2 chain could not be verified due to a temporary DNS error"
}
})
}
}
fn top_signature<'x>(
message: &'x AuthenticatedMessage<'x>,
) -> Option<(&'x String, &'x str, &'x [String])> {
message
.dkim2_signatures
.iter()
.map(|h| &h.header)
.max_by_key(|s| s.i)
.map(envelope_of)
}
fn envelope_of(signature: &Signature) -> (&String, &str, &[String]) {
match &signature.chain {
ChainBinding::Envelope { mail_from, rcpt_to } => {
(&signature.d, mail_from.as_str(), rcpt_to.as_slice())
}
ChainBinding::NextDomain(_) => (&signature.d, "", &[][..]),
}
}
fn domain_of(address: &str) -> &str {
let address = address.trim_start_matches('<').trim_end_matches('>');
address.rsplit_once('@').map(|(_, d)| d).unwrap_or(address)
}
fn is_dkim2_signed(message: &AuthenticatedMessage<'_>) -> bool {
!message.dkim2_signatures.is_empty() || message.has_dkim2_errors
}
impl AuthenticatedMessage<'_> {
pub fn dkim2_return_path(&self) -> Option<&str> {
dsn_return_path(self.dkim2_signatures.iter().map(|header| &header.header))
}
pub fn dkim2_dsn_domains(&self) -> impl Iterator<Item = &str> {
top_signature(self)
.map_or(&[][..], |(_, _, rcpt_to)| rcpt_to)
.iter()
.map(|rcpt| domain_of(rcpt))
.filter(|domain| !domain.starts_with('['))
.flat_map(|domain| {
std::iter::successors(Some(domain), |domain| {
domain.split_once('.').map(|(_, parent)| parent)
})
.filter(|domain| domain.contains('.'))
})
}
pub fn dkim2_dsn_aligned(&self, domain: &str) -> bool {
top_signature(self).is_none_or(|(_, _, rcpt_to)| {
rcpt_to.is_empty()
|| rcpt_to
.iter()
.any(|rcpt| relaxed_domain_match(domain_of(rcpt), domain))
})
}
pub fn dkim2_arrival_headers(&self, hops: usize) -> Vec<u8> {
let signatures = &self.dkim2_signatures;
let kept = signatures
.get(..signatures.len().saturating_sub(hops))
.unwrap_or_default();
let arrival_m = kept.last().map_or(0, |signature| signature.header.m);
let (older, newer) = self.dkim2_instances.split_at(
self.dkim2_instances
.partition_point(|instance| instance.header.m <= arrival_m),
);
let mut out = Vec::with_capacity(self.raw_headers().len());
for (name, value) in kept
.iter()
.map(|header| (header.name, header.value))
.chain(older.iter().map(|header| (header.name, header.value)))
{
out.extend_from_slice(name);
out.push(b':');
out.extend_from_slice(value);
}
let mut undone: Option<Vec<u8>> = None;
for recipe in newer
.iter()
.rev()
.filter_map(|instance| instance.header.recipe.as_ref())
{
let mut previous = Vec::with_capacity(self.raw_headers().len());
match &undone {
Some(block) => recipe.apply_headers(
&HeaderIterator::new(block).collect::<Vec<_>>(),
&mut previous,
),
None => recipe.apply_headers(self.headers(), &mut previous),
}
undone = Some(previous);
}
match undone {
Some(block) => out.extend_from_slice(&block),
None => Recipe::default().apply_headers(self.headers(), &mut out),
}
out
}
}
fn dsn_return_path<'x>(signatures: impl Iterator<Item = &'x Signature>) -> Option<&'x str> {
signatures
.max_by_key(|s| s.i)
.and_then(|top| match &top.chain {
ChainBinding::Envelope { mail_from, .. }
if !mail_from.is_empty() && mail_from != "<>" =>
{
Some(mail_from.as_str())
}
_ => None,
})
}
impl MessageAuthenticator {
pub async fn verify_dkim2_dsn<'x, R, S, C, A, RT>(
&self,
params: impl Into<Parameters<'x, &'x Dkim2Dsn<'x, R, S>, C>>,
envelope: Envelope<A, RT>,
) -> Result<Dkim2DsnOutput, Dkim2DsnFailure>
where
R: AsRef<AuthenticatedMessage<'x>> + 'x,
S: AsRef<AuthenticatedMessage<'x>> + 'x,
C: DnsCache + 'x,
A: AsRef<str> + Clone,
RT: IntoIterator<Item: AsRef<str>> + Clone,
{
let params = params.into();
self.verify_dkim2_dsn_(params.input, envelope, params.txt_cache(), now())
.await
}
pub(crate) async fn verify_dkim2_dsn_<'x, R, S, TXT, A, RT>(
&self,
dsn: &'x Dkim2Dsn<'x, R, S>,
envelope: Envelope<A, RT>,
cache_txt: Option<&TXT>,
now: u64,
) -> Result<Dkim2DsnOutput, Dkim2DsnFailure>
where
R: AsRef<AuthenticatedMessage<'x>> + 'x,
S: AsRef<AuthenticatedMessage<'x>> + 'x,
TXT: ResolverCache<Box<str>, TxtRecord>,
A: AsRef<str> + Clone,
RT: IntoIterator<Item: AsRef<str>> + Clone,
{
let dsn_message = dsn.dsn.as_ref();
if !is_dkim2_signed(dsn_message) {
return Err(Dkim2DsnFailure::DsnNotSigned);
} else if !is_dkim2_signed(dsn.returned.as_ref()) {
return Err(Dkim2DsnFailure::ReturnedNotSigned);
}
let dsn_output = self
.verify_dkim2_(
dsn_message,
envelope.clone(),
cache_txt,
now,
true,
Verification::Full,
)
.await;
let dsn_result = dsn_output.result;
match dsn_result {
Dkim2Result::Pass => {}
Dkim2Result::TempError(_) => return Err(Dkim2DsnFailure::TempError),
_ => return Err(Dkim2DsnFailure::DsnChainFailed),
}
let dsn_signing_domain = dsn_message
.dkim2_signatures
.iter()
.map(|h| &h.header)
.find(|signature| signature.i == 1)
.map(|signature| &signature.d);
let returned_top = top_signature(dsn.returned.as_ref());
let (returned_mail_from, returned_rcpt_to) = returned_top
.as_ref()
.map(|(_, mail_from, rcpt_to)| (*mail_from, *rcpt_to))
.unwrap_or_default();
let returned_result = self
.verify_dkim2_(
dsn.returned.as_ref(),
Envelope::new(returned_mail_from, returned_rcpt_to),
cache_txt,
now,
dsn.returned_full,
Verification::Full,
)
.await
.result;
match returned_result {
Dkim2Result::Pass => {}
Dkim2Result::TempError(_) => return Err(Dkim2DsnFailure::TempError),
_ => return Err(Dkim2DsnFailure::ReturnedChainFailed),
}
let aligned = match (&dsn_signing_domain, &returned_top) {
(Some(dsn_domain), Some((returned_domain, _, rcpt_to))) => {
let recipient_aligned = rcpt_to
.iter()
.any(|rcpt| relaxed_domain_match(domain_of(rcpt), dsn_domain));
let Envelope {
rcpt_to: envelope_rcpt_to,
..
} = envelope;
let returned_is_ours = envelope_rcpt_to
.into_iter()
.any(|rcpt| relaxed_domain_match(domain_of(rcpt.as_ref()), returned_domain));
recipient_aligned && returned_is_ours
}
_ => false,
};
if aligned {
Ok(Dkim2DsnOutput {
dsn: dsn_result,
returned: returned_result,
})
} else {
Err(Dkim2DsnFailure::NotAligned)
}
}
}
impl<'x, R, S> From<&'x Dkim2Dsn<'x, R, S>> for Parameters<'x, &'x Dkim2Dsn<'x, R, S>, NoCache>
where
R: AsRef<AuthenticatedMessage<'x>>,
S: AsRef<AuthenticatedMessage<'x>>,
{
fn from(params: &'x Dkim2Dsn<'x, R, S>) -> Self {
Parameters::new(params)
}
}
#[cfg(test)]
mod test {
use super::{Dkim2Dsn, Dkim2DsnFailure, Dkim2DsnOutput, Recipe, Signature, dsn_return_path};
use crate::{
AuthenticatedMessage, DnsError, Error, MessageAuthenticator,
crypto::Ed25519Key,
dkim::DomainKey,
dkim2::{ChainBinding, Dkim2Signer, Envelope, Hop},
dns::{TxtRecord, cache::test::DummyCaches},
parse::TxtRecordParser,
};
use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
use std::{
path::PathBuf,
time::{Duration, Instant},
};
const NOW: u64 = 1740002100;
const T: u64 = 1740000000;
fn resource(parts: &[&str]) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources/dkim2");
for part in parts {
path.push(part);
}
path
}
fn load_key(domain: &str, selector: &str) -> Ed25519Key {
let pem = std::fs::read(resource(&[
"keys",
&format!("{selector}._domainkey.{domain}.pem"),
]))
.unwrap();
let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
panic!("expected PKCS8 key");
};
Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
}
fn load_caches() -> DummyCaches {
let caches = DummyCaches::new();
let dns = std::fs::read(resource(&["dns.json"])).unwrap();
let dns: serde_json::Value = serde_json::from_slice(&dns).unwrap();
let valid_until = Instant::now() + Duration::new(3600, 0);
for (domain, selectors) in dns.as_object().unwrap() {
for (selector, records) in selectors.as_object().unwrap() {
caches.txt_add(
format!("{selector}.{domain}."),
DomainKey::parse(records[0][1].as_str().unwrap().as_bytes()).unwrap(),
valid_until,
);
}
}
caches
}
fn sign_full<A, R, I>(
key: Ed25519Key,
domain: &str,
selector: &str,
message: &[u8],
hop: Hop<A, R, I>,
) -> Vec<u8>
where
A: AsRef<str>,
R: IntoIterator<Item: AsRef<str>>,
I: Into<String>,
{
let signer = Dkim2Signer::from_key(key).domain(domain).selector(selector);
let signed = signer.sign_at(message, hop, None, T).unwrap();
let mut out = signed.to_header().into_bytes();
out.extend_from_slice(message);
out
}
const RETURNED_PLAIN: &str = concat!(
"From: sender@test1.dkim2.com\r\n",
"To: user@test2.dkim2.com\r\n",
"Subject: Hello\r\n",
"Date: Sat, 01 Mar 2026 12:00:00 +0000\r\n",
"Message-ID: <m@test1.dkim2.com>\r\n",
"\r\n",
"This is the original body.\r\n",
);
fn signed_returned() -> Vec<u8> {
sign_full(
load_key("test1.dkim2.com", "ed25519"),
"test1.dkim2.com",
"ed25519",
RETURNED_PLAIN.as_bytes(),
Hop::real("sender@test1.dkim2.com", ["user@test2.dkim2.com"]),
)
}
fn headers_only(message: &[u8]) -> Vec<u8> {
let end = message.windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
message[..end].to_vec()
}
fn make_dsn(returned: &[u8], returned_ct: &str, dsn_signed: bool) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: text/plain\r\n\r\n");
body.extend_from_slice(b"Delivery to user@test2.dkim2.com failed.\r\n");
body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: message/delivery-status\r\n\r\n");
body.extend_from_slice(b"Reporting-MTA: dns; test2.dkim2.com\r\n\r\n");
body.extend_from_slice(b"Final-Recipient: rfc822; user@test2.dkim2.com\r\n");
body.extend_from_slice(b"Action: failed\r\nStatus: 5.1.1\r\n");
body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: ");
body.extend_from_slice(returned_ct.as_bytes());
body.extend_from_slice(b"\r\n\r\n");
body.extend_from_slice(returned);
body.extend_from_slice(b"\r\n--BOUNDARY--\r\n");
let mut dsn_plain = Vec::new();
dsn_plain.extend_from_slice(b"From: postmaster@test2.dkim2.com\r\n");
dsn_plain.extend_from_slice(b"To: sender@test1.dkim2.com\r\n");
dsn_plain.extend_from_slice(b"Subject: Delivery Status Notification (Failure)\r\n");
dsn_plain.extend_from_slice(b"Date: Sat, 01 Mar 2026 12:05:00 +0000\r\n");
dsn_plain.extend_from_slice(
b"Content-Type: multipart/report; report-type=delivery-status; boundary=\"BOUNDARY\"\r\n",
);
dsn_plain.extend_from_slice(b"\r\n");
dsn_plain.extend_from_slice(&body);
if dsn_signed {
sign_full(
load_key("test2.dkim2.com", "ed25519"),
"test2.dkim2.com",
"ed25519",
&dsn_plain,
Hop::real("<>", ["sender@test1.dkim2.com"]),
)
} else {
dsn_plain
}
}
async fn verify(dsn_bytes: &[u8]) -> Result<Dkim2DsnOutput, Dkim2DsnFailure> {
verify_with(dsn_bytes, load_caches()).await
}
async fn verify_with(
dsn_bytes: &[u8],
caches: DummyCaches,
) -> Result<Dkim2DsnOutput, Dkim2DsnFailure> {
let resolver = MessageAuthenticator::new_system_conf().unwrap();
let dsn = Dkim2Dsn::parse(dsn_bytes).expect("parse DSN");
let params = caches.parameters(&dsn);
let envelope = Envelope::new("<>", ["sender@test1.dkim2.com"]);
resolver
.verify_dkim2_dsn_(&dsn, envelope, params.txt_cache(), NOW)
.await
}
#[test]
fn dsn_return_path_null() {
let mut signature = Signature {
i: 1,
chain: ChainBinding::Envelope {
mail_from: "<>".to_string(),
rcpt_to: vec!["recipient@example.com".to_string()],
},
..Default::default()
};
assert_eq!(dsn_return_path(std::iter::once(&signature)), None);
signature.chain = ChainBinding::Envelope {
mail_from: "sender@test1.dkim2.com".to_string(),
rcpt_to: vec!["recipient@example.com".to_string()],
};
assert_eq!(
dsn_return_path(std::iter::once(&signature)),
Some("sender@test1.dkim2.com")
);
}
fn forwarded(modify_subject: bool) -> Vec<u8> {
let returned = signed_returned();
let text = String::from_utf8(returned.clone()).unwrap();
let modified = if modify_subject {
text.replacen("Subject: Hello", "Subject: [fwd] Hello", 1)
} else {
text
};
let signed = Dkim2Signer::from_key(load_key("test2.dkim2.com", "ed25519"))
.domain("test2.dkim2.com")
.selector("ed25519")
.out_of_band_arrangement()
.sign_revised_at(
returned.as_slice(),
modified.as_bytes(),
Hop::real("user@test2.dkim2.com", ["final@example.com"]),
None,
T,
)
.unwrap();
let mut out = signed.to_header().into_bytes();
out.extend_from_slice(modified.as_bytes());
out
}
#[test]
fn dsn_signing_domains() {
let returned = signed_returned();
let message = AuthenticatedMessage::parse(&returned).unwrap();
assert_eq!(
message.dkim2_dsn_domains().collect::<Vec<_>>(),
["test2.dkim2.com", "dkim2.com"]
);
assert!(message.dkim2_dsn_aligned("test2.dkim2.com"));
assert!(message.dkim2_dsn_aligned("DKIM2.com"));
assert!(!message.dkim2_dsn_aligned("sub.test2.dkim2.com"));
assert!(!message.dkim2_dsn_aligned("test1.dkim2.com"));
let unsigned = AuthenticatedMessage::parse(RETURNED_PLAIN.as_bytes()).unwrap();
assert_eq!(unsigned.dkim2_dsn_domains().count(), 0);
assert!(unsigned.dkim2_dsn_aligned("anything.example"));
}
#[test]
fn dsn_arrival_headers() {
let previous = signed_returned();
let previous = AuthenticatedMessage::parse(&previous).unwrap();
let mut expected = Vec::new();
Recipe::default().apply_headers(previous.headers(), &mut expected);
for modify_subject in [false, true] {
let forwarded = forwarded(modify_subject);
let message = AuthenticatedMessage::parse(&forwarded).unwrap();
assert_eq!(message.dkim2_signatures().len(), 2);
let arrival = message.dkim2_arrival_headers(1);
assert!(arrival.ends_with(&expected), "{modify_subject}");
let arrival = AuthenticatedMessage::parse(&arrival).unwrap();
assert_eq!(
arrival
.dkim2_signatures()
.iter()
.map(|signature| signature.header.i)
.collect::<Vec<_>>(),
[1]
);
assert_eq!(
arrival
.dkim2_instances()
.iter()
.map(|instance| instance.header.m)
.collect::<Vec<_>>(),
[1]
);
assert!(arrival.dkim2_dsn_aligned("test2.dkim2.com"));
let unchanged = AuthenticatedMessage::parse(&forwarded).unwrap();
let all = unchanged.dkim2_arrival_headers(0);
let all = AuthenticatedMessage::parse(&all).unwrap();
assert_eq!(all.dkim2_signatures().len(), 2);
assert_eq!(
all.dkim2_instances().len(),
if modify_subject { 2 } else { 1 }
);
let none = unchanged.dkim2_arrival_headers(2);
let none = AuthenticatedMessage::parse(&none).unwrap();
assert!(none.dkim2_signatures().is_empty());
assert!(none.dkim2_instances().is_empty());
assert!(
none.headers()
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(b"From"))
);
}
}
#[tokio::test]
async fn verify_dsn_with_two_own_hops() {
let returned = signed_returned();
let hop =
|message: &[u8], domain: &str, from: &str, to: &str, find: &str, replace: &str| {
let text = String::from_utf8(message.to_vec()).unwrap();
let modified = text.replacen(find, replace, 1);
let signed = Dkim2Signer::from_key(load_key(domain, "ed25519"))
.domain(domain)
.selector("ed25519")
.out_of_band_arrangement()
.sign_revised_at(message, modified.as_bytes(), Hop::real(from, [to]), None, T)
.unwrap();
let mut out = signed.to_header().into_bytes();
out.extend_from_slice(modified.as_bytes());
out
};
let hop2 = hop(
&returned,
"test2.dkim2.com",
"user@test2.dkim2.com",
"relay@test3.dkim2.com",
"Subject: Hello",
"Subject: [list] Hello",
);
let hop3 = hop(
&hop2,
"test3.dkim2.com",
"relay@test3.dkim2.com",
"final@example.com",
"Subject: [list] Hello",
"Subject: [list] [fwd] Hello",
);
let message = AuthenticatedMessage::parse(&hop3).unwrap();
assert_eq!(message.dkim2_signatures().len(), 3);
assert_eq!(message.dkim2_instances().len(), 3);
let arrival = message.dkim2_arrival_headers(2);
assert!(!String::from_utf8_lossy(&arrival).contains("[list]"));
let dsn = make_dsn(&arrival, "text/rfc822-headers", true);
let output = verify(&dsn).await;
assert!(output.is_ok(), "{output:?}");
}
#[tokio::test]
async fn verify_dsn_with_arrival_headers() {
for modify_subject in [false, true] {
let forwarded = forwarded(modify_subject);
let message = AuthenticatedMessage::parse(&forwarded).unwrap();
let dsn = make_dsn(
&message.dkim2_arrival_headers(1),
"text/rfc822-headers",
true,
);
let output = verify(&dsn).await;
assert!(output.is_ok(), "{modify_subject}: {output:?}");
let dsn = make_dsn(&headers_only(&forwarded), "text/rfc822-headers", true);
assert_eq!(verify(&dsn).await, Err(Dkim2DsnFailure::NotAligned));
}
}
#[tokio::test]
async fn verify_dsn_round_trip() {
let dsn_signed = make_dsn(&signed_returned(), "message/rfc822", true);
assert!(Dkim2Dsn::parse(&dsn_signed).unwrap().returned_full);
let output = verify(&dsn_signed).await;
assert!(output.is_ok(), "{output:?}");
}
#[tokio::test]
async fn verify_dsn_with_two_signatures() {
let plain = make_dsn(&signed_returned(), "message/rfc822", false);
let hop1 = sign_full(
load_key("test2.dkim2.com", "ed25519"),
"test2.dkim2.com",
"ed25519",
&plain,
Hop::real("<>", ["relay@test3.dkim2.com"]),
);
let signed = Dkim2Signer::from_key(load_key("test3.dkim2.com", "ed25519"))
.domain("test3.dkim2.com")
.selector("ed25519")
.out_of_band_arrangement()
.sign_at(
hop1.as_slice(),
Hop::real("<>", ["sender@test1.dkim2.com"]),
None,
T,
)
.unwrap();
let mut hop2 = signed.to_header().into_bytes();
hop2.extend_from_slice(&hop1);
let output = verify(&hop2).await;
assert!(output.is_ok(), "{output:?}");
}
#[tokio::test]
async fn verify_dsn_returned_headers_only() {
let dsn_signed = make_dsn(
&headers_only(&signed_returned()),
"text/rfc822-headers",
true,
);
assert!(!Dkim2Dsn::parse(&dsn_signed).unwrap().returned_full);
let output = verify(&dsn_signed).await;
assert!(output.is_ok(), "{output:?}");
}
#[tokio::test]
async fn verify_dsn_returned_not_signed() {
let dsn_signed = make_dsn(RETURNED_PLAIN.as_bytes(), "message/rfc822", true);
let output = verify(&dsn_signed).await;
assert_eq!(output, Err(Dkim2DsnFailure::ReturnedNotSigned));
}
#[tokio::test]
async fn verify_dsn_temporary_dns_failure() {
let dsn_signed = make_dsn(&signed_returned(), "message/rfc822", true);
for domain in ["test2.dkim2.com", "test1.dkim2.com"] {
let caches = load_caches();
caches.txt_add(
format!("ed25519._domainkey.{domain}."),
TxtRecord::Error(Error::Dns(DnsError::Resolver("timeout".to_string()))),
Instant::now() + Duration::new(3600, 0),
);
assert_eq!(
verify_with(&dsn_signed, caches).await,
Err(Dkim2DsnFailure::TempError),
"{domain}"
);
}
}
#[tokio::test]
async fn verify_dsn_not_signed() {
let dsn_unsigned = make_dsn(&signed_returned(), "message/rfc822", false);
let output = verify(&dsn_unsigned).await;
assert_eq!(output, Err(Dkim2DsnFailure::DsnNotSigned));
}
}