Return-Path, DSNs and VERP: Where Bounces Actually Go
How the envelope sender decides where a bounce is delivered, what a delivery status notification contains, why bounces use an empty sender, and how VERP tells you exactly which recipient failed.
Published
Every message carries two sender addresses. The one people see is the From: header inside the message. The one mail servers act on is the envelope sender, given in the MAIL FROM command of the SMTP conversation. When delivery fails, the failure report goes to the envelope sender. That single fact explains most of the confusion about “where did the bounces go?”
The envelope sender and the Return-Path header
During the SMTP transaction your sending server says, in effect:
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
The address in MAIL FROM is the envelope sender, also called the return-path or bounce address. It never has to match the From: header, and for bulk mail it usually does not. When the message reaches its final mailbox, the receiving server records the envelope sender as a Return-Path: header at the top of the message. That is why you can open any delivered message and see where its bounces would have gone.
Two consequences follow:
- Bounces do not go to the From address. If your newsletter is sent from
[email protected]with an envelope sender of[email protected], a failure report goes to the second address. If nothing reads that mailbox, the bounce is lost and the dead address stays on your list. - SPF checks the envelope sender’s domain. The SPF lookup is done for
mail.example.comin the example above, not forexample.com. For DMARC, that domain has to align with the From domain, which is why a return-path on your own subdomain matters.
Two ways a bounce comes back
A failure reaches you in one of two ways, and bounce processing has to handle both.
In the SMTP session. The receiving server refuses the recipient or the message while your server is still connected, with a reply such as 550 5.1.1. Your MTA sees the code immediately. Depending on how it is set up, it logs the failure, reports it to your application, or generates a bounce message to the envelope sender itself.
After the session. The receiving server accepts the message (250) and only later finds it cannot deliver it, for example because a downstream server rejects it or a forwarding address is dead. It then builds a delivery status notification (DSN) and mails it to the envelope sender.
Receivers increasingly prefer the first route. Accepting a message and bouncing it later means sending a new message to whatever address was in MAIL FROM, and for spam that address is usually forged. The bounces then land on innocent third parties, which is known as backscatter. Well-run servers check recipients and policy during the session and reject there, so the only party that hears about the failure is the server that tried to send.
What a DSN contains
A standard DSN (RFC 3464) is a multipart/report message with three parts:
- A human-readable explanation.
- A machine-readable
message/delivery-statuspart. - The original message, or just its headers.
The middle part is the one bounce processing should read:
Reporting-MTA: dns; mx.example.org
Final-Recipient: rfc822; [email protected]
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 User unknown
Action says whether the message failed, was delayed, or was relayed onward. Status carries the enhanced status code from RFC 3463, the same class.subject.detail triple described in the bounce code guide. Diagnostic-Code holds the remote server’s own reply, word for word.
Not every bounce is this tidy. Some servers still send free-text failure messages without a delivery-status part, and some auto-replies look like bounces. A bounce processor that trusts only the Status field will miss some failures; one that greps the text for “failed” will misread vacation replies. Read the structured part when it exists and fall back to the reply code in the text when it does not.
Why bounces are sent from an empty sender
A DSN is itself sent with an empty envelope sender:
MAIL FROM:<>
The empty path, called the null sender, means “do not bounce this”. If the DSN cannot be delivered either, it is dropped rather than generating another DSN, which prevents two servers from bouncing failure reports back and forth for ever. It also means your bounce mailbox must accept mail from the null sender. Filters that reject MAIL FROM:<> as suspicious quietly throw away every bounce you would have received.
VERP: knowing exactly who bounced
Parsing a DSN to find the failed recipient is unreliable. The Final-Recipient field may show an internal alias, a forwarded address, or nothing usable at all. Variable envelope return path (VERP) avoids the problem by giving every recipient their own envelope sender:
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
Your bounce mailbox accepts anything addressed to bounces+…@mail.example.com. When a bounce arrives at [email protected], the failed recipient is encoded in the address it was delivered to, whatever the DSN body says. Many senders put a message or campaign identifier in the same place, so they can tie a bounce to the send that caused it.
The cost is that each recipient now needs its own SMTP transaction, because one MAIL FROM can no longer cover many recipients at the same domain. For newsletters, where messages are often personalised anyway, that cost is small.
Retries, queue lifetime and the delayed DSN
A temporary 4xx failure does not produce a bounce straight away. The message stays in your MTA’s queue and is retried. RFC 5321 suggests retrying no more often than every 30 minutes or so and giving up after at least four to five days. Many MTAs also send a “delayed” DSN (Action: delayed) after a few hours so the sender knows mail is stuck.
For bulk mail those defaults are often too generous. A daily newsletter that arrives three days late is worse than one that never arrives, and keeping it in the queue costs connections and reputation at a receiver that is already deferring you. Many senders run separate queues: a short lifetime for newsletters, the full window for receipts and password resets. Whatever you choose, the final failure after the queue expires should be treated as a soft bounce for that address, not a hard one. The address may be fine; the receiver may simply have been busy or throttling you.
Where this leaves your bounce handling
The return path is the only channel by which receiving servers can tell you something went wrong. Keep it on your own domain, make sure a program reads it, accept the null sender, and encode the recipient in the address so you never have to guess who bounced. Then feed hard failures into the suppression list your app respects.