NAME
Mail::DKIM2::DSN - DKIM2-signed Delivery Status Notifications
SYNOPSIS
use Mail::DKIM2::DSN;
# A DSN for a message we are bouncing, signed with MAIL FROM <>.
my $out = Mail::DKIM2::DSN->generate(
Message => $inbound_bytes,
Signer => Mail::DKIM2::Signer->new(..., MailFrom => '<>'),
ReportingMTA => 'mx.example.com',
Status => '5.7.1',
Reason => 'rejected by policy',
);
# $out->{raw}, $out->{send_to}
# Is this DSN about a message we forwarded, and genuine?
my $auth = Mail::DKIM2::DSN->authenticate(
Message => $dsn_bytes,
PubkeyCallback => \&lookup, # or omit to use DNS
);
# $auth->{ok}, $auth->{top} (our signature), $auth->{alignment}
# Send it on towards the original sender.
my $out = Mail::DKIM2::DSN->propagate(
Message => $dsn_bytes,
ForwarderDomain => 'fwd.example',
Signer => $signer, # MailFrom => '<>'
PubkeyCallback => \&lookup,
);
# $out->{raw}, $out->{upstream_mailfrom}
DESCRIPTION
-07 section 16 describes how a DSN (RFC 3464, structured per RFC 6522) is signed, checked and passed back along the chain. A forwarder that receives a DSN for a message it forwarded rebuilds the enclosed original to the state it was in when it went out (undoing its own Message-Instance and removing the signature and instance it added), then re-signs the whole DSN as a new message with MAIL FROM <>, so it carries exactly one Message-Instance and one DKIM2-Signature.
This module implements draft-ietf-dkim-dkim2-spec-07; see "STATUS" in Mail::DKIM2 for what that means for the wire format and the API, and "CONVENTIONS" in Mail::DKIM2 for the option, input and error conventions every module here follows.
CLASS METHODS
Each takes named arguments and returns a hashref. Each croaks on a message that is not an RFC 6522 DSN or on a missing required argument.
generate(%args)
Builds a three-part multipart/report DSN returning Message, signed by Signer (which must have MailFrom => '<>'), From postmaster@ the signer's Domain (-07 section 11). To is the envelope sender to bounce to; if omitted it is the top DKIM2-Signature's mf= (never the From: header field, as before 0.23). ReportingMTA, Status (default 5.7.1) and Reason fill the delivery-status part. Returns { raw => $bytes, send_to => $address }.
Croaks, sending nothing, unless may_generate allows it: never for a message whose top mf= is <>, nor for one that is not Provisionally Verified (-07 section 16), checked with PubkeyCallback or Resolver (DNS when neither is given) and SkipTimestampCheck, which are new in 0.23.
may_generate(%args)
Whether a DSN may be sent for Message at all: its top DKIM2-Signature's mf= is not <>, and it is Provisionally Verified (see "provisionally_verified()" in Mail::DKIM2::Verifier), with keys from PubkeyCallback or Resolver and SkipTimestampCheck as above. Returns (1, undef), or (0, $why). New in 0.23.
authenticate(%args)
Checks Message, a DSN, per -07 section 16.3: the returned original's chain verifies (from its headers alone if that is all the DSN carries), the DSN's own signature verifies, and the DSN's d= is aligned with the returned original's top rt=. Keys come from PubkeyCallback if given, else from DNS through Resolver (a Net::DNS::Resolver is made if neither is given); SkipTimestampCheck is passed to the verifiers too. Returns a hashref with ok; top, the returned original's highest signature, for the caller to recognise as its own by d= and mf=; result, details, dsn_result, dsn_details, dsn_sig; alignment (pass, fail or none) and alignment_detail; headers_only; and embedded, the returned original as an Email::MIME.
What ok covers: the returned original's chain verified, the DSN's own signature did not fail, and alignment did not fail. A DSN that carries no DKIM2-Signature of its own (dsn_result none, as from an MTA that does not sign its bounces) is still ok when the returned original verifies -- so ok alone shows the bounce is about a genuine message, not that the bounce itself is signed. A caller that requires a signed bounce checks dsn_result eq 'pass' as well. Neither says the message was one this host sent: compare top's d= and mf= with your own.
propagate(%args)
Authenticates Message as above (pass SkipAuthentication => 1 if the caller already has), then rebuilds and re-signs it as described. Croaks rather than propagate a DSN that does not authenticate: -07 section 16.3 says such a DSN MUST NOT be propagated; PubkeyCallback and Resolver are passed to authenticate. ForwarderDomain is the d= of the hop to strip, and must be the d= of the returned original's top DKIM2-Signature: propagate croaks rather than strip another domain's hop. Croaks rather than propagate towards a null mf= (<>, -07 section 16). The propagated DSN is From postmaster@ the signer's Domain. Returns { raw => $bytes, upstream_mailfrom => $address }.
AUTHOR
Bron Gondwana <brong@fastmailteam.com>
COPYRIGHT AND LICENSE
Copyright (c) 2025-2026 Fastmail Pty Ltd. This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.