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.