# 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.

Source: https://pidgely.com/blog/verp-bounce-handling/ · Published: 2026-10-12 · Pidgely

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:

| 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

```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.

