Hi Bron,
Thank you for clearly splitting this into "originate at the edge" and
"propagate an inbound DSN". And routing bounce(8) through the milter
(your `internal_mail_filter_classes = bounce` suggestion) answers most
of what I was stuck on.
*There's one case that seems to sit between your cases 1 and 2, that I
want to make sure I've got right, though.*
Imagine me as a forwarder or a list. I accept a message (I say 250),
make my changes, add my Message-Instance and signature, and relay it on.
The next hop rejects it right there in the session with a 5xx. No DSN
ever comes back to me (correct?) — my own MTA has to generate the bounce
to my previous hop, because I already took responsibility when I said 250.
If that bounce goes through the milter, my code sees it — good. But it's
not the same job as your case 1. For a message that failed at me, I just
sign the bounce fresh. For this one, the bounce is about a message I
modified and signed, so before I sign it I also have to undo my recipe
and strip my signature, so the original goes back the way my previous
hop sent it to me.
So my question is really: is the milter expected to notice the
difference and do that undo on the bounce path? And two practical worries:
1. Does Postfix put enough of the original message in the bounce for me
to undo it, or only the headers? (If only headers, I fall back to a
headers-only DSN, which the spec allows.). This is something that can be
toggled in Postfix, but one needs to be aware of it.
2. Doing the undo means the bounce milter is now rewriting the message,
not just signing it. You were careful to say sign-only is the safe mode
on non_smtpd_milters — is a rewrite-in-place on the bounce path still
safe by your reckoning, or does that reopen the loop concern?
Thanks again — this is genuinely helping me understand DKIM2 (and
mailflow) better, and get PhoenixDKIM right (thank you for looking at it!).
Kind regards,
Edmund
P.S.: Tangentically: On the `disable_mime_output_conversion point`: It
made me think of recent a convo on the Postfix mailing list about IDN2
and how milters can become footguns there, too. See:
https://www.mail-archive.com/[email protected]/msg108087.html
and the rest of the convo.
On 2026/07/02 23:32, Bron Gondwana wrote:
Replying to the clean one!
There are two bits of DSN handling - here's some detail, partly me,
partly my Claude instance which did most of the implementation and
testing.
*1. Originating a DSN for a message that failed at your edge. *
This is a brand-new DKIM2 message: MAIL FROM <>, one Message-Instance,
one DKIM2-Signature (§12). There is no recipe to undo — it's a fresh
message, so this is just "sign an outbound message," which a milter
can do, provided the milter runs over the MTA's own generated bounces.
On Postfix that's internal_mail_filter_classes = bounce, which routes
bounce(8) output through non_smtpd_milters. We do exactly this on the
demo server (the [email protected] address).
Two caveats from experience:
* Postfix's "generally not safe" warning is aimed at content filters
that re-inject or rewrite and can loop. A sign-only milter on
non_smtpd_milters is fine — it doesn't re-inject.
* You must stop the MTA from re-encoding the message after signing,
or the signature breaks. We were bitten by the 8bit→7bit downgrade
rewriting Content-Transfer-Encoding on a bounce after the milter
signed it; disable_mime_output_conversion = yes fixes it. (This is
the "Preventing Transport Conversions" section of the spec, and
it's worth flagging that MTA-originated bounces are genuinely
harder here than normal outbound mail.)
o I do think a patch to postfix to allow or prefer 7 bit
bounce(8) generation makes sense, there's no need to put
Content-Transfer-Encoding to 8 bit where there's no 8 bit in
the message.
*2. Propagating a DSN back up a forwarding chain (§12.1). *
When you forwarded a message and it bounced further downstream, you
receive a DSN, undo your recipe, drop your signature, and re-emit to
the previous hop. This is where the undo/re-sign logic lives — but
note it's triggered by an inbound message, arriving over SMTP, so
there's no async-bounce problem: a milter/filter sees it, and your
library does the undo + re-sign. It's ordinary inbound processing, not
the bounce(8) path.
So the recipe-undoing you were worried about never applies to the
MTA's own freshly-generated bounce; it only applies to case 2, which
isn't in the async path anyway.
On the pipe transport: yes, that's what the reference implementation
does for its reflector-dsn address — a pipe(8) transport that shells
out to the DKIM2 library and re-injects on a milter-free port. It's
convenient for the propagation case and for a demo, but you're right
that it's fragile and per-MTA. It's a stopgap, not the intended end
state. The point of doing it with a pipe + a milter today is to prove
the whole thing is implementable on an unmodified MTA and to let the
implementations interoperate now. The expectation is absolutely that
DKIM2 support lands natively in the MTAs (Postfix/Exim/Sendmail) — the
same path DKIM1 took from milter to native — and that operators
shouldn't have to hand-roll a pipe per box.
*For PhoenixDKIM specifically (milter + library), Claude suggests:*
* Sign your MTA's own bounces in the milter via
internal_mail_filter_classes = bounce + non_smtpd_milters (sign
only), plus disable_mime_output_conversion = yes. No per-operator
pipe needed.
* Handle DSN propagation (undo + re-sign) as ordinary inbound
processing in your library; it's an inbound SMTP message, not a
bounce-path callback.
Bron.
On Thu, Jun 25, 2026, at 06:00, Edmund Lodewijks wrote:
~~ Sending this again, as the previous message got horribly mangled!
Apologies for the noise! ~~
Hello all!
I am trying to implement §12 of spec-03 in PhoenixDKIM (an OpenDKIM fork
— milter plus library), and my question is where DSN generation is
supposed to live.
A milter cannot do it, I think: Once we've answered `250`, the session
is gone and there is no milter in the async bounce path. And the MTA's
own bounce generator knows nothing about DKIM2— it won't undo recipe
changes or sign the DSN as a fresh message with `mf=<>`.
It was suggested earlier on this ML (Bron?) that you can do this on
Postfix today without core changes, presumably a pipe/ transport that
shells out to a DKIM2 library and re-injects. This seems to be what the
reference implementation on github is doing?
That would work, but is the expectation indeed that every operator wires
their own pipe, per MTA? That feels fragile and inefficient, if everyone
is supposed to go that way with DKIM2. Is the intent that this
eventually lands in the MTAs (Postfix/ Exim/ Sendmail), with the pipe
just a stopgap?
Thank you for your time, sharing of knowledge, and energy!
Kind regards,
Edmund Lodewijks
--
Edmund Lodewijks <[email protected]>
TZ: UTC+2 / GMT+2
_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]
--
Bron Gondwana, CEO, Fastmail Pty Ltd / Fastmail US LLC
[email protected]
_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]
--
Edmund Lodewijks <[email protected]>
TZ: UTC+2 / GMT+2
_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]