Skip to content
Pidgely
  • deliverability
  • bounces
  • smtp

VERP and Automated Bounce Handling: DSN Parsing Explained

How VERP encodes the recipient in the return-path, how to parse RFC 3464 DSNs, and how to tell real bounces from auto-replies in an automated bounce pipeline.

Pidgely teamPublished 5 min readMarkdown version

On this page
  1. Why bounce handling is harder than it looks
  2. How VERP works
  3. Anatomy of a DSN
  4. Bounces vs auto-replies vs everything else
  5. A processing pipeline
  6. Common mistakes
  7. Checklist
  8. Frequently asked questions

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:

text
MAIL FROM:<bounces@example.com>

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

text
MAIL FROM:<b-01HZX4Q7M2-jane=example.net@bounces.example.com>

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:

text
MAIL FROM:<b-7f3k9q2xv1@bounces.example.com>

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:

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

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.

text
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--

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:

MessageHow to recognize itAction
DSN failuremultipart/report; report-type=delivery-status, Action: failedClassify by Status, maybe suppress
DSN delaySame, Action: delayedRecord, do not suppress
Auto-replyAuto-Submitted: auto-replied (RFC 3834), X-Autoreply, Precedence auto_reply, normal content typeIgnore for suppression
Challenge-responseAsks you to verify you are humanIgnore
Non-standard bouncePlain text with phrases like "user unknown", from MAILER-DAEMON or postmasterHeuristic parsing, conservative actions
Complaint (ARF)multipart/report; report-type=feedback-report (RFC 5965)Route to complaint handling
Human replyAnything elseForward 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

text
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

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.

Frequently asked questions

What is VERP?

VERP (Variable Envelope Return Path) is a technique where each message gets a unique envelope sender address that encodes the recipient or message ID, such as bounce-a1b2c3@bounces.example.com. Any bounce sent back to that address identifies the failed recipient without parsing the bounce body.

Does VERP break SPF or DMARC?

No, as long as the return-path domain is covered by your SPF record and aligns with the From domain. Only the local part varies, so a subdomain such as bounces.example.com still aligns under relaxed DMARC alignment.

How do I tell a bounce from an out-of-office reply?

A real bounce is usually a multipart/report message with report-type=delivery-status (RFC 3464), sent with an empty envelope sender. Auto-replies are normal messages, often marked with Auto-Submitted: auto-replied (RFC 3834), and must never be treated as bounces.

What fields in a DSN matter most?

In the per-recipient section, Final-Recipient or Original-Recipient identifies the address, Action (failed, delayed, delivered, relayed, expanded) gives the outcome, Status is the enhanced status code, and Diagnostic-Code contains the remote server's reply.

Pidgely team

Published

We build Pidgely, the email API that gets you to the inbox. We write about what we learn delivering email, citing the standards and mailbox provider guidelines behind every claim.

Also available in: