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->{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
Spec-06 section 12 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-06; 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 => '<>'). To is the envelope sender to bounce to; if omitted it is taken from the top DKIM2-Signature's mf=, failing that from From:. ReportingMTA, Status (default 5.7.1) and Reason fill the delivery-status part. Returns { raw => $bytes, to => $address }.
authenticate(%args)
Checks Message, a DSN, per section 12.1.2: 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=. PubkeyCallback, Resolver and SkipTimestampCheck are passed to the verifiers. 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.
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: section 12.1.2 says such a DSN MUST NOT be propagated. ForwarderDomain is the d= of the hop to strip. 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.