1use super::{
11 ChainBinding, Dkim2Result, Recipe, Signature, Verification, sign::Envelope,
12 verify::relaxed_domain_match,
13};
14use crate::dns::DnsCache;
15use crate::headers::HeaderIterator;
16use crate::{
17 AuthenticatedMessage, MessageAuthenticator, Parameters, ResolverCache, TxtRecord,
18 dkim2::sign::now, dns::NoCache,
19};
20use mail_parser::MessageParser;
21use std::marker::PhantomData;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Dkim2Dsn<'x, R = AuthenticatedMessage<'x>, S = AuthenticatedMessage<'x>>
30where
31 R: AsRef<AuthenticatedMessage<'x>>,
32 S: AsRef<AuthenticatedMessage<'x>>,
33{
34 pub dsn: R,
36 pub returned: S,
38 pub returned_full: bool,
42 _marker: PhantomData<&'x ()>,
43}
44
45impl<'x, R, S> Dkim2Dsn<'x, R, S>
46where
47 R: AsRef<AuthenticatedMessage<'x>>,
48 S: AsRef<AuthenticatedMessage<'x>>,
49{
50 pub fn new(dsn: R, returned: S, returned_full: bool) -> Self {
53 Dkim2Dsn {
54 dsn,
55 returned,
56 returned_full,
57 _marker: PhantomData,
58 }
59 }
60}
61
62impl<'x> Dkim2Dsn<'x> {
63 pub fn parse(raw_message: &'x [u8]) -> Result<Dkim2Dsn<'x>, Dkim2DsnFailure> {
74 let message = MessageParser::new()
75 .parse(raw_message)
76 .ok_or(Dkim2DsnFailure::DsnUnparseable)?;
77 let root = message.root_part();
78 if !root.is_multipart() {
79 return Err(Dkim2DsnFailure::DsnUnparseable);
80 }
81
82 let mut returned = None;
83 for part in root.children() {
84 let slice = raw_message
85 .get(part.offset_body() as usize..part.offset_end() as usize)
86 .ok_or(Dkim2DsnFailure::DsnUnparseable)?;
87 if part.is_content_type("message", "rfc822") {
88 returned = Some((slice, true));
89 } else if part.is_content_type("text", "rfc822-headers") {
90 returned = Some((slice, false));
91 }
92 }
93
94 let (returned_slice, returned_full) =
95 returned.ok_or(Dkim2DsnFailure::ReturnedUnparseable)?;
96 Ok(Dkim2Dsn {
97 dsn: AuthenticatedMessage::parse(raw_message).ok_or(Dkim2DsnFailure::DsnUnparseable)?,
98 returned: AuthenticatedMessage::parse(returned_slice)
99 .ok_or(Dkim2DsnFailure::ReturnedUnparseable)?,
100 returned_full,
101 _marker: PhantomData,
102 })
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct Dkim2DsnOutput {
110 pub dsn: Dkim2Result,
112 pub returned: Dkim2Result,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum Dkim2DsnFailure {
119 DsnUnparseable,
121 ReturnedUnparseable,
123 DsnNotSigned,
126 DsnChainFailed,
128 ReturnedNotSigned,
130 ReturnedChainFailed,
132 NotAligned,
135 TempError,
139}
140
141impl std::fmt::Display for Dkim2DsnFailure {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.write_str(match self {
144 Dkim2DsnFailure::DsnUnparseable => "DSN could not be parsed",
145 Dkim2DsnFailure::ReturnedUnparseable => "Returned message could not be parsed",
146 Dkim2DsnFailure::DsnNotSigned => "DSN is not DKIM2 signed",
147 Dkim2DsnFailure::DsnChainFailed => "DSN signature chain failed",
148 Dkim2DsnFailure::ReturnedNotSigned => "Returned message is not DKIM2 signed",
149 Dkim2DsnFailure::ReturnedChainFailed => "Returned message signature chain failed",
150 Dkim2DsnFailure::NotAligned => {
151 "DSN signer is not aligned with the returned message recipient"
152 }
153 Dkim2DsnFailure::TempError => {
154 "DKIM2 chain could not be verified due to a temporary DNS error"
155 }
156 })
157 }
158}
159
160fn top_signature<'x>(
161 message: &'x AuthenticatedMessage<'x>,
162) -> Option<(&'x String, &'x str, &'x [String])> {
163 message
164 .dkim2_signatures
165 .iter()
166 .map(|h| &h.header)
167 .max_by_key(|s| s.i)
168 .map(envelope_of)
169}
170
171fn envelope_of(signature: &Signature) -> (&String, &str, &[String]) {
172 match &signature.chain {
173 ChainBinding::Envelope { mail_from, rcpt_to } => {
174 (&signature.d, mail_from.as_str(), rcpt_to.as_slice())
175 }
176 ChainBinding::NextDomain(_) => (&signature.d, "", &[][..]),
177 }
178}
179
180fn domain_of(address: &str) -> &str {
181 let address = address.trim_start_matches('<').trim_end_matches('>');
182 address.rsplit_once('@').map(|(_, d)| d).unwrap_or(address)
183}
184
185fn is_dkim2_signed(message: &AuthenticatedMessage<'_>) -> bool {
186 !message.dkim2_signatures.is_empty() || message.has_dkim2_errors
187}
188
189impl AuthenticatedMessage<'_> {
190 pub fn dkim2_return_path(&self) -> Option<&str> {
199 dsn_return_path(self.dkim2_signatures.iter().map(|header| &header.header))
200 }
201
202 pub fn dkim2_dsn_domains(&self) -> impl Iterator<Item = &str> {
204 top_signature(self)
205 .map_or(&[][..], |(_, _, rcpt_to)| rcpt_to)
206 .iter()
207 .map(|rcpt| domain_of(rcpt))
208 .filter(|domain| !domain.starts_with('['))
209 .flat_map(|domain| {
210 std::iter::successors(Some(domain), |domain| {
211 domain.split_once('.').map(|(_, parent)| parent)
212 })
213 .filter(|domain| domain.contains('.'))
214 })
215 }
216
217 pub fn dkim2_dsn_aligned(&self, domain: &str) -> bool {
219 top_signature(self).is_none_or(|(_, _, rcpt_to)| {
220 rcpt_to.is_empty()
221 || rcpt_to
222 .iter()
223 .any(|rcpt| relaxed_domain_match(domain_of(rcpt), domain))
224 })
225 }
226
227 pub fn dkim2_arrival_headers(&self, hops: usize) -> Vec<u8> {
230 let signatures = &self.dkim2_signatures;
231 let kept = signatures
232 .get(..signatures.len().saturating_sub(hops))
233 .unwrap_or_default();
234 let arrival_m = kept.last().map_or(0, |signature| signature.header.m);
235 let (older, newer) = self.dkim2_instances.split_at(
236 self.dkim2_instances
237 .partition_point(|instance| instance.header.m <= arrival_m),
238 );
239
240 let mut out = Vec::with_capacity(self.raw_headers().len());
241 for (name, value) in kept
242 .iter()
243 .map(|header| (header.name, header.value))
244 .chain(older.iter().map(|header| (header.name, header.value)))
245 {
246 out.extend_from_slice(name);
247 out.push(b':');
248 out.extend_from_slice(value);
249 }
250
251 let mut undone: Option<Vec<u8>> = None;
252 for recipe in newer
253 .iter()
254 .rev()
255 .filter_map(|instance| instance.header.recipe.as_ref())
256 {
257 let mut previous = Vec::with_capacity(self.raw_headers().len());
258 match &undone {
259 Some(block) => recipe.apply_headers(
260 &HeaderIterator::new(block).collect::<Vec<_>>(),
261 &mut previous,
262 ),
263 None => recipe.apply_headers(self.headers(), &mut previous),
264 }
265 undone = Some(previous);
266 }
267 match undone {
268 Some(block) => out.extend_from_slice(&block),
269 None => Recipe::default().apply_headers(self.headers(), &mut out),
270 }
271 out
272 }
273}
274
275fn dsn_return_path<'x>(signatures: impl Iterator<Item = &'x Signature>) -> Option<&'x str> {
276 signatures
277 .max_by_key(|s| s.i)
278 .and_then(|top| match &top.chain {
279 ChainBinding::Envelope { mail_from, .. }
280 if !mail_from.is_empty() && mail_from != "<>" =>
281 {
282 Some(mail_from.as_str())
283 }
284 _ => None,
285 })
286}
287
288impl MessageAuthenticator {
289 pub async fn verify_dkim2_dsn<'x, R, S, C, A, RT>(
351 &self,
352 params: impl Into<Parameters<'x, &'x Dkim2Dsn<'x, R, S>, C>>,
353 envelope: Envelope<A, RT>,
354 ) -> Result<Dkim2DsnOutput, Dkim2DsnFailure>
355 where
356 R: AsRef<AuthenticatedMessage<'x>> + 'x,
357 S: AsRef<AuthenticatedMessage<'x>> + 'x,
358 C: DnsCache + 'x,
359 A: AsRef<str> + Clone,
360 RT: IntoIterator<Item: AsRef<str>> + Clone,
361 {
362 let params = params.into();
363 self.verify_dkim2_dsn_(params.input, envelope, params.txt_cache(), now())
364 .await
365 }
366
367 pub(crate) async fn verify_dkim2_dsn_<'x, R, S, TXT, A, RT>(
368 &self,
369 dsn: &'x Dkim2Dsn<'x, R, S>,
370 envelope: Envelope<A, RT>,
371 cache_txt: Option<&TXT>,
372 now: u64,
373 ) -> Result<Dkim2DsnOutput, Dkim2DsnFailure>
374 where
375 R: AsRef<AuthenticatedMessage<'x>> + 'x,
376 S: AsRef<AuthenticatedMessage<'x>> + 'x,
377 TXT: ResolverCache<Box<str>, TxtRecord>,
378 A: AsRef<str> + Clone,
379 RT: IntoIterator<Item: AsRef<str>> + Clone,
380 {
381 let dsn_message = dsn.dsn.as_ref();
382 if !is_dkim2_signed(dsn_message) {
383 return Err(Dkim2DsnFailure::DsnNotSigned);
384 } else if !is_dkim2_signed(dsn.returned.as_ref()) {
385 return Err(Dkim2DsnFailure::ReturnedNotSigned);
386 }
387
388 let dsn_output = self
389 .verify_dkim2_(
390 dsn_message,
391 envelope.clone(),
392 cache_txt,
393 now,
394 true,
395 Verification::Full,
396 )
397 .await;
398 let dsn_result = dsn_output.result;
399 match dsn_result {
400 Dkim2Result::Pass => {}
401 Dkim2Result::TempError(_) => return Err(Dkim2DsnFailure::TempError),
402 _ => return Err(Dkim2DsnFailure::DsnChainFailed),
403 }
404
405 let dsn_signing_domain = dsn_message
406 .dkim2_signatures
407 .iter()
408 .map(|h| &h.header)
409 .find(|signature| signature.i == 1)
410 .map(|signature| &signature.d);
411 let returned_top = top_signature(dsn.returned.as_ref());
412 let (returned_mail_from, returned_rcpt_to) = returned_top
413 .as_ref()
414 .map(|(_, mail_from, rcpt_to)| (*mail_from, *rcpt_to))
415 .unwrap_or_default();
416 let returned_result = self
417 .verify_dkim2_(
418 dsn.returned.as_ref(),
419 Envelope::new(returned_mail_from, returned_rcpt_to),
420 cache_txt,
421 now,
422 dsn.returned_full,
423 Verification::Full,
424 )
425 .await
426 .result;
427 match returned_result {
428 Dkim2Result::Pass => {}
429 Dkim2Result::TempError(_) => return Err(Dkim2DsnFailure::TempError),
430 _ => return Err(Dkim2DsnFailure::ReturnedChainFailed),
431 }
432
433 let aligned = match (&dsn_signing_domain, &returned_top) {
434 (Some(dsn_domain), Some((returned_domain, _, rcpt_to))) => {
435 let recipient_aligned = rcpt_to
436 .iter()
437 .any(|rcpt| relaxed_domain_match(domain_of(rcpt), dsn_domain));
438
439 let Envelope {
440 rcpt_to: envelope_rcpt_to,
441 ..
442 } = envelope;
443 let returned_is_ours = envelope_rcpt_to
444 .into_iter()
445 .any(|rcpt| relaxed_domain_match(domain_of(rcpt.as_ref()), returned_domain));
446
447 recipient_aligned && returned_is_ours
448 }
449 _ => false,
450 };
451
452 if aligned {
453 Ok(Dkim2DsnOutput {
454 dsn: dsn_result,
455 returned: returned_result,
456 })
457 } else {
458 Err(Dkim2DsnFailure::NotAligned)
459 }
460 }
461}
462
463impl<'x, R, S> From<&'x Dkim2Dsn<'x, R, S>> for Parameters<'x, &'x Dkim2Dsn<'x, R, S>, NoCache>
464where
465 R: AsRef<AuthenticatedMessage<'x>>,
466 S: AsRef<AuthenticatedMessage<'x>>,
467{
468 fn from(params: &'x Dkim2Dsn<'x, R, S>) -> Self {
469 Parameters::new(params)
470 }
471}
472
473#[cfg(test)]
474mod test {
475 use super::{Dkim2Dsn, Dkim2DsnFailure, Dkim2DsnOutput, Recipe, Signature, dsn_return_path};
476 use crate::{
477 AuthenticatedMessage, DnsError, Error, MessageAuthenticator,
478 crypto::Ed25519Key,
479 dkim::DomainKey,
480 dkim2::{ChainBinding, Dkim2Signer, Envelope, Hop},
481 dns::{TxtRecord, cache::test::DummyCaches},
482 parse::TxtRecordParser,
483 };
484 use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
485 use std::{
486 path::PathBuf,
487 time::{Duration, Instant},
488 };
489
490 const NOW: u64 = 1740002100;
491 const T: u64 = 1740000000;
492
493 fn resource(parts: &[&str]) -> PathBuf {
494 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
495 path.push("resources/dkim2");
496 for part in parts {
497 path.push(part);
498 }
499 path
500 }
501
502 fn load_key(domain: &str, selector: &str) -> Ed25519Key {
503 let pem = std::fs::read(resource(&[
504 "keys",
505 &format!("{selector}._domainkey.{domain}.pem"),
506 ]))
507 .unwrap();
508 let PrivateKeyDer::Pkcs8(der) = PrivateKeyDer::from_pem_slice(&pem).unwrap() else {
509 panic!("expected PKCS8 key");
510 };
511 Ed25519Key::from_pkcs8_maybe_unchecked_der(der.secret_pkcs8_der()).unwrap()
512 }
513
514 fn load_caches() -> DummyCaches {
515 let caches = DummyCaches::new();
516 let dns = std::fs::read(resource(&["dns.json"])).unwrap();
517 let dns: serde_json::Value = serde_json::from_slice(&dns).unwrap();
518 let valid_until = Instant::now() + Duration::new(3600, 0);
519 for (domain, selectors) in dns.as_object().unwrap() {
520 for (selector, records) in selectors.as_object().unwrap() {
521 caches.txt_add(
522 format!("{selector}.{domain}."),
523 DomainKey::parse(records[0][1].as_str().unwrap().as_bytes()).unwrap(),
524 valid_until,
525 );
526 }
527 }
528 caches
529 }
530
531 fn sign_full<A, R, I>(
532 key: Ed25519Key,
533 domain: &str,
534 selector: &str,
535 message: &[u8],
536 hop: Hop<A, R, I>,
537 ) -> Vec<u8>
538 where
539 A: AsRef<str>,
540 R: IntoIterator<Item: AsRef<str>>,
541 I: Into<String>,
542 {
543 let signer = Dkim2Signer::from_key(key).domain(domain).selector(selector);
544 let signed = signer.sign_at(message, hop, None, T).unwrap();
545 let mut out = signed.to_header().into_bytes();
546 out.extend_from_slice(message);
547 out
548 }
549
550 const RETURNED_PLAIN: &str = concat!(
551 "From: sender@test1.dkim2.com\r\n",
552 "To: user@test2.dkim2.com\r\n",
553 "Subject: Hello\r\n",
554 "Date: Sat, 01 Mar 2026 12:00:00 +0000\r\n",
555 "Message-ID: <m@test1.dkim2.com>\r\n",
556 "\r\n",
557 "This is the original body.\r\n",
558 );
559
560 fn signed_returned() -> Vec<u8> {
561 sign_full(
562 load_key("test1.dkim2.com", "ed25519"),
563 "test1.dkim2.com",
564 "ed25519",
565 RETURNED_PLAIN.as_bytes(),
566 Hop::real("sender@test1.dkim2.com", ["user@test2.dkim2.com"]),
567 )
568 }
569
570 fn headers_only(message: &[u8]) -> Vec<u8> {
571 let end = message.windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
572 message[..end].to_vec()
573 }
574
575 fn make_dsn(returned: &[u8], returned_ct: &str, dsn_signed: bool) -> Vec<u8> {
576 let mut body = Vec::new();
577 body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: text/plain\r\n\r\n");
578 body.extend_from_slice(b"Delivery to user@test2.dkim2.com failed.\r\n");
579 body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: message/delivery-status\r\n\r\n");
580 body.extend_from_slice(b"Reporting-MTA: dns; test2.dkim2.com\r\n\r\n");
581 body.extend_from_slice(b"Final-Recipient: rfc822; user@test2.dkim2.com\r\n");
582 body.extend_from_slice(b"Action: failed\r\nStatus: 5.1.1\r\n");
583 body.extend_from_slice(b"--BOUNDARY\r\nContent-Type: ");
584 body.extend_from_slice(returned_ct.as_bytes());
585 body.extend_from_slice(b"\r\n\r\n");
586 body.extend_from_slice(returned);
587 body.extend_from_slice(b"\r\n--BOUNDARY--\r\n");
588
589 let mut dsn_plain = Vec::new();
590 dsn_plain.extend_from_slice(b"From: postmaster@test2.dkim2.com\r\n");
591 dsn_plain.extend_from_slice(b"To: sender@test1.dkim2.com\r\n");
592 dsn_plain.extend_from_slice(b"Subject: Delivery Status Notification (Failure)\r\n");
593 dsn_plain.extend_from_slice(b"Date: Sat, 01 Mar 2026 12:05:00 +0000\r\n");
594 dsn_plain.extend_from_slice(
595 b"Content-Type: multipart/report; report-type=delivery-status; boundary=\"BOUNDARY\"\r\n",
596 );
597 dsn_plain.extend_from_slice(b"\r\n");
598 dsn_plain.extend_from_slice(&body);
599
600 if dsn_signed {
601 sign_full(
602 load_key("test2.dkim2.com", "ed25519"),
603 "test2.dkim2.com",
604 "ed25519",
605 &dsn_plain,
606 Hop::real("<>", ["sender@test1.dkim2.com"]),
607 )
608 } else {
609 dsn_plain
610 }
611 }
612
613 async fn verify(dsn_bytes: &[u8]) -> Result<Dkim2DsnOutput, Dkim2DsnFailure> {
614 verify_with(dsn_bytes, load_caches()).await
615 }
616
617 async fn verify_with(
618 dsn_bytes: &[u8],
619 caches: DummyCaches,
620 ) -> Result<Dkim2DsnOutput, Dkim2DsnFailure> {
621 let resolver = MessageAuthenticator::new_system_conf().unwrap();
622 let dsn = Dkim2Dsn::parse(dsn_bytes).expect("parse DSN");
623 let params = caches.parameters(&dsn);
624 let envelope = Envelope::new("<>", ["sender@test1.dkim2.com"]);
625 resolver
626 .verify_dkim2_dsn_(&dsn, envelope, params.txt_cache(), NOW)
627 .await
628 }
629
630 #[test]
631 fn dsn_return_path_null() {
632 let mut signature = Signature {
633 i: 1,
634 chain: ChainBinding::Envelope {
635 mail_from: "<>".to_string(),
636 rcpt_to: vec!["recipient@example.com".to_string()],
637 },
638 ..Default::default()
639 };
640 assert_eq!(dsn_return_path(std::iter::once(&signature)), None);
641
642 signature.chain = ChainBinding::Envelope {
643 mail_from: "sender@test1.dkim2.com".to_string(),
644 rcpt_to: vec!["recipient@example.com".to_string()],
645 };
646 assert_eq!(
647 dsn_return_path(std::iter::once(&signature)),
648 Some("sender@test1.dkim2.com")
649 );
650 }
651
652 fn forwarded(modify_subject: bool) -> Vec<u8> {
653 let returned = signed_returned();
654 let text = String::from_utf8(returned.clone()).unwrap();
655 let modified = if modify_subject {
656 text.replacen("Subject: Hello", "Subject: [fwd] Hello", 1)
657 } else {
658 text
659 };
660 let signed = Dkim2Signer::from_key(load_key("test2.dkim2.com", "ed25519"))
661 .domain("test2.dkim2.com")
662 .selector("ed25519")
663 .out_of_band_arrangement()
664 .sign_revised_at(
665 returned.as_slice(),
666 modified.as_bytes(),
667 Hop::real("user@test2.dkim2.com", ["final@example.com"]),
668 None,
669 T,
670 )
671 .unwrap();
672 let mut out = signed.to_header().into_bytes();
673 out.extend_from_slice(modified.as_bytes());
674 out
675 }
676
677 #[test]
679 fn dsn_signing_domains() {
680 let returned = signed_returned();
681 let message = AuthenticatedMessage::parse(&returned).unwrap();
682 assert_eq!(
683 message.dkim2_dsn_domains().collect::<Vec<_>>(),
684 ["test2.dkim2.com", "dkim2.com"]
685 );
686 assert!(message.dkim2_dsn_aligned("test2.dkim2.com"));
687 assert!(message.dkim2_dsn_aligned("DKIM2.com"));
688 assert!(!message.dkim2_dsn_aligned("sub.test2.dkim2.com"));
689 assert!(!message.dkim2_dsn_aligned("test1.dkim2.com"));
690
691 let unsigned = AuthenticatedMessage::parse(RETURNED_PLAIN.as_bytes()).unwrap();
692 assert_eq!(unsigned.dkim2_dsn_domains().count(), 0);
693 assert!(unsigned.dkim2_dsn_aligned("anything.example"));
694 }
695
696 #[test]
698 fn dsn_arrival_headers() {
699 let previous = signed_returned();
700 let previous = AuthenticatedMessage::parse(&previous).unwrap();
701 let mut expected = Vec::new();
702 Recipe::default().apply_headers(previous.headers(), &mut expected);
703
704 for modify_subject in [false, true] {
705 let forwarded = forwarded(modify_subject);
706 let message = AuthenticatedMessage::parse(&forwarded).unwrap();
707 assert_eq!(message.dkim2_signatures().len(), 2);
708
709 let arrival = message.dkim2_arrival_headers(1);
710 assert!(arrival.ends_with(&expected), "{modify_subject}");
711 let arrival = AuthenticatedMessage::parse(&arrival).unwrap();
712 assert_eq!(
713 arrival
714 .dkim2_signatures()
715 .iter()
716 .map(|signature| signature.header.i)
717 .collect::<Vec<_>>(),
718 [1]
719 );
720 assert_eq!(
721 arrival
722 .dkim2_instances()
723 .iter()
724 .map(|instance| instance.header.m)
725 .collect::<Vec<_>>(),
726 [1]
727 );
728 assert!(arrival.dkim2_dsn_aligned("test2.dkim2.com"));
729
730 let unchanged = AuthenticatedMessage::parse(&forwarded).unwrap();
731 let all = unchanged.dkim2_arrival_headers(0);
732 let all = AuthenticatedMessage::parse(&all).unwrap();
733 assert_eq!(all.dkim2_signatures().len(), 2);
734 assert_eq!(
735 all.dkim2_instances().len(),
736 if modify_subject { 2 } else { 1 }
737 );
738
739 let none = unchanged.dkim2_arrival_headers(2);
740 let none = AuthenticatedMessage::parse(&none).unwrap();
741 assert!(none.dkim2_signatures().is_empty());
742 assert!(none.dkim2_instances().is_empty());
743 assert!(
744 none.headers()
745 .iter()
746 .any(|(name, _)| name.eq_ignore_ascii_case(b"From"))
747 );
748 }
749 }
750
751 #[tokio::test]
753 async fn verify_dsn_with_two_own_hops() {
754 let returned = signed_returned();
755 let hop =
756 |message: &[u8], domain: &str, from: &str, to: &str, find: &str, replace: &str| {
757 let text = String::from_utf8(message.to_vec()).unwrap();
758 let modified = text.replacen(find, replace, 1);
759 let signed = Dkim2Signer::from_key(load_key(domain, "ed25519"))
760 .domain(domain)
761 .selector("ed25519")
762 .out_of_band_arrangement()
763 .sign_revised_at(message, modified.as_bytes(), Hop::real(from, [to]), None, T)
764 .unwrap();
765 let mut out = signed.to_header().into_bytes();
766 out.extend_from_slice(modified.as_bytes());
767 out
768 };
769 let hop2 = hop(
770 &returned,
771 "test2.dkim2.com",
772 "user@test2.dkim2.com",
773 "relay@test3.dkim2.com",
774 "Subject: Hello",
775 "Subject: [list] Hello",
776 );
777 let hop3 = hop(
778 &hop2,
779 "test3.dkim2.com",
780 "relay@test3.dkim2.com",
781 "final@example.com",
782 "Subject: [list] Hello",
783 "Subject: [list] [fwd] Hello",
784 );
785 let message = AuthenticatedMessage::parse(&hop3).unwrap();
786 assert_eq!(message.dkim2_signatures().len(), 3);
787 assert_eq!(message.dkim2_instances().len(), 3);
788
789 let arrival = message.dkim2_arrival_headers(2);
790 assert!(!String::from_utf8_lossy(&arrival).contains("[list]"));
791 let dsn = make_dsn(&arrival, "text/rfc822-headers", true);
792 let output = verify(&dsn).await;
793 assert!(output.is_ok(), "{output:?}");
794 }
795
796 #[tokio::test]
798 async fn verify_dsn_with_arrival_headers() {
799 for modify_subject in [false, true] {
800 let forwarded = forwarded(modify_subject);
801 let message = AuthenticatedMessage::parse(&forwarded).unwrap();
802
803 let dsn = make_dsn(
804 &message.dkim2_arrival_headers(1),
805 "text/rfc822-headers",
806 true,
807 );
808 let output = verify(&dsn).await;
809 assert!(output.is_ok(), "{modify_subject}: {output:?}");
810
811 let dsn = make_dsn(&headers_only(&forwarded), "text/rfc822-headers", true);
812 assert_eq!(verify(&dsn).await, Err(Dkim2DsnFailure::NotAligned));
813 }
814 }
815
816 #[tokio::test]
817 async fn verify_dsn_round_trip() {
818 let dsn_signed = make_dsn(&signed_returned(), "message/rfc822", true);
819 assert!(Dkim2Dsn::parse(&dsn_signed).unwrap().returned_full);
820
821 let output = verify(&dsn_signed).await;
822 assert!(output.is_ok(), "{output:?}");
823 }
824
825 #[tokio::test]
827 async fn verify_dsn_with_two_signatures() {
828 let plain = make_dsn(&signed_returned(), "message/rfc822", false);
829 let hop1 = sign_full(
830 load_key("test2.dkim2.com", "ed25519"),
831 "test2.dkim2.com",
832 "ed25519",
833 &plain,
834 Hop::real("<>", ["relay@test3.dkim2.com"]),
835 );
836 let signed = Dkim2Signer::from_key(load_key("test3.dkim2.com", "ed25519"))
837 .domain("test3.dkim2.com")
838 .selector("ed25519")
839 .out_of_band_arrangement()
840 .sign_at(
841 hop1.as_slice(),
842 Hop::real("<>", ["sender@test1.dkim2.com"]),
843 None,
844 T,
845 )
846 .unwrap();
847 let mut hop2 = signed.to_header().into_bytes();
848 hop2.extend_from_slice(&hop1);
849
850 let output = verify(&hop2).await;
851 assert!(output.is_ok(), "{output:?}");
852 }
853
854 #[tokio::test]
855 async fn verify_dsn_returned_headers_only() {
856 let dsn_signed = make_dsn(
857 &headers_only(&signed_returned()),
858 "text/rfc822-headers",
859 true,
860 );
861 assert!(!Dkim2Dsn::parse(&dsn_signed).unwrap().returned_full);
862
863 let output = verify(&dsn_signed).await;
864 assert!(output.is_ok(), "{output:?}");
865 }
866
867 #[tokio::test]
868 async fn verify_dsn_returned_not_signed() {
869 let dsn_signed = make_dsn(RETURNED_PLAIN.as_bytes(), "message/rfc822", true);
870
871 let output = verify(&dsn_signed).await;
872 assert_eq!(output, Err(Dkim2DsnFailure::ReturnedNotSigned));
873 }
874
875 #[tokio::test]
877 async fn verify_dsn_temporary_dns_failure() {
878 let dsn_signed = make_dsn(&signed_returned(), "message/rfc822", true);
879 for domain in ["test2.dkim2.com", "test1.dkim2.com"] {
880 let caches = load_caches();
881 caches.txt_add(
882 format!("ed25519._domainkey.{domain}."),
883 TxtRecord::Error(Error::Dns(DnsError::Resolver("timeout".to_string()))),
884 Instant::now() + Duration::new(3600, 0),
885 );
886 assert_eq!(
887 verify_with(&dsn_signed, caches).await,
888 Err(Dkim2DsnFailure::TempError),
889 "{domain}"
890 );
891 }
892 }
893
894 #[tokio::test]
895 async fn verify_dsn_not_signed() {
896 let dsn_unsigned = make_dsn(&signed_returned(), "message/rfc822", false);
897
898 let output = verify(&dsn_unsigned).await;
899 assert_eq!(output, Err(Dkim2DsnFailure::DsnNotSigned));
900 }
901}