DEV Community

cendyandreoli
cendyandreoli

Posted on Originally published at pidgely.com

VERP and Automated Bounce Handling: DSN Parsing Explained

VERP (Variable Envelope Return Path) is a bounce-handling technique in which every message is sent with a unique envelope sender, such as bounce-7f3k9q@bounces.example.com, that encodes which recipient or message it belongs to. When a delivery failure comes back, the address it was sent to tells you exactly which recipient failed, even if the bounce body is malformed. A complete automated pipeline combines VERP for identification, RFC 3464 Delivery Status Notification (DSN) parsing for the reason, and filters that separate real bounces from auto-replies and other noise.

Why bounce handling is harder than it looks

Synchronous failures are easy: your MTA receives 550 5.1.1 User unknown during the SMTP session and records it. The hard part is asynchronous bounces. A server accepts the message, then later finds it cannot deliver it and sends a new message back to the envelope sender.

Those messages are inconsistent. Many follow RFC 3464, but some servers send free-text bounces, some rewrite the recipient after forwarding, and some report an internal alias rather than the address you sent to. If you rely on parsing the human-readable text to find the recipient, you will mismatch or miss bounces. VERP removes that guesswork.

How VERP works

Without VERP, all mail uses one return-path:

MAIL FROM:<bounces@example.com>
Enter fullscreen mode Exit fullscreen mode

With VERP, the local part becomes unique per recipient or per message:

MAIL FROM:<b-01HZX4Q7M2-jane=example.net@bounces.example.com>
Enter fullscreen mode Exit fullscreen mode

The classic VERP format, as introduced for qmail, embeds the recipient address with @ replaced by =. Modern systems usually prefer an opaque token instead, which does not leak the recipient address and keeps the local part short:

MAIL FROM:<b-7f3k9q2xv1@bounces.example.com>
Enter fullscreen mode Exit fullscreen mode

The token maps (in a database or through an HMAC-signed encoding) to the message ID, the recipient and the stream. When a bounce arrives at b-7f3k9q2xv1@bounces.example.com, you look up the token and know exactly what failed.

DNS for the bounce domain

The return-path domain must receive mail and authorize your sending IPs:

bounces.example.com.  IN MX   10 inbound.example.com.
bounces.example.com.  IN TXT  "v=spf1 ip4:203.0.113.0/28 -all"
Enter fullscreen mode Exit fullscreen mode

Because it is a subdomain of example.com, SPF aligns with a From: ...@example.com address under DMARC relaxed alignment (RFC 7489). VERP and DMARC work together without conflict.

Token design tips

  • Keep the local part under 64 octets (the RFC 5321 limit).
  • Use only lowercase letters and digits; some systems lowercase or mangle case and punctuation.
  • Sign tokens or make them unguessable, so attackers cannot forge bounces that unsubscribe your users.
  • Keep the mapping for as long as bounces can arrive (days, not minutes).

Anatomy of a DSN

A standards-compliant bounce is a multipart/report message (RFC 6522) with report-type=delivery-status. It is sent with an empty envelope sender (MAIL FROM:<>) so that bounces never generate more bounces.

Return-Path: <>
To: b-7f3k9q2xv1@bounces.example.com
Subject: Undelivered Mail Returned to Sender
Content-Type: multipart/report; report-type=delivery-status; boundary="XYZ"

--XYZ
Content-Type: text/plain

The following address failed: jane@example.net

--XYZ
Content-Type: message/delivery-status

Reporting-MTA: dns; mx1.example.net
Arrival-Date: Mon, 12 Oct 2026 14:03:20 +0000

Final-Recipient: rfc822; jane@example.net
Original-Recipient: rfc822; jane@example.net
Action: failed
Status: 5.1.1
Remote-MTA: dns; mailstore3.example.net
Diagnostic-Code: smtp; 550 5.1.1 <jane@example.net>: Recipient address rejected: User unknown

--XYZ
Content-Type: message/rfc822-headers

Message-ID: <01HZX4Q7M2@mail.example.com>
From: Example <hello@example.com>
--XYZ--
Enter fullscreen mode Exit fullscreen mode

The fields defined by RFC 3464 that matter:

  • Action: failed, delayed, delivered, relayed or expanded. Only failed is a bounce. delayed is a warning that the remote server is still retrying.
  • Status: the enhanced status code (RFC 3463), for example 5.1.1 or 4.2.2.
  • Diagnostic-Code: the remote server's actual reply, useful for classification and debugging.
  • Final-Recipient and Original-Recipient: which address failed. With VERP you use these to cross-check, not as the source of truth.

The third part, either message/rfc822 or message/rfc822-headers, contains the original message or its headers. Your own Message-ID there is a second way to correlate.

Bounces vs auto-replies vs everything else

The return-path address receives more than bounces. Classify every inbound message before acting:

Message How to recognize it Action
DSN failure multipart/report; report-type=delivery-status, Action: failed Classify by Status, maybe suppress
DSN delay Same, Action: delayed Record, do not suppress
Auto-reply Auto-Submitted: auto-replied (RFC 3834), X-Autoreply, Precedence auto_reply, normal content type Ignore for suppression
Challenge-response Asks you to verify you are human Ignore
Non-standard bounce Plain text with phrases like "user unknown", from MAILER-DAEMON or postmaster Heuristic parsing, conservative actions
Complaint (ARF) multipart/report; report-type=feedback-report (RFC 5965) Route to complaint handling
Human reply Anything else Forward to a mailbox or drop by policy

Never suppress an address because of an out-of-office reply. The mailbox exists, and the person will be back.

A processing pipeline

inbound SMTP (bounces.example.com)
  -> accept; store raw message
  -> resolve VERP token -> (message_id, recipient, stream)
  -> classify: dsn | arf | auto-reply | unknown
  -> dsn: parse per-recipient fields; for each Action=failed
         -> classify Status: address | transient | policy
         -> address: add to suppression list (global)
         -> transient: count toward soft-bounce threshold
         -> policy: record + alert, no suppression
  -> emit event (bounced / delayed / complained) to webhooks
Enter fullscreen mode Exit fullscreen mode

Implementation notes:

  • Store the raw message before parsing, so you can reprocess when you improve the parser.
  • Make processing idempotent: the same bounce can arrive twice, and retries happen.
  • Handle multiple recipients in one DSN; each recipient has its own section.
  • Fall back gracefully when the DSN is not standard: if the VERP token resolves but the body cannot be parsed, record an "unknown failure" rather than guessing a hard bounce.
  • Reject unknown tokens with a 550 or accept and discard. Do not let your bounce domain become an open relay or backscatter target.

Common mistakes

  • One static return-path and regex parsing of bounce bodies to find the recipient.
  • Treating Action: delayed as a bounce.
  • Suppressing on auto-replies or challenge-response messages.
  • Return-path on a domain that does not align with the From domain, which breaks SPF alignment for DMARC.
  • Forgeable tokens that let anyone suppress arbitrary recipients.
  • Short token retention, so late bounces cannot be matched.

Checklist

  • [ ] Unique, signed VERP token per message or recipient
  • [ ] Bounce subdomain with MX and SPF, aligned with From
  • [ ] RFC 3464 parser handling multiple recipients and all Action values
  • [ ] Separate paths for DSNs, ARF reports and auto-replies
  • [ ] Raw message storage and idempotent processing
  • [ ] Address, transient and policy failures handled differently

Pidgely uses VERP on a custom return-path subdomain for every message, parses DSNs and ARF reports on its own inbound servers, and turns them into bounce and complaint events with automatic suppression.


Originally published at pidgely.com. Pidgely is a transactional and marketing email API with built-in bounce handling: pidgely.com.

Top comments (0)