Introduction
VERP (Variable Envelope Return Path) is a technique where each outgoing email uses a unique envelope sender address that encodes the recipient's address. When a bounce is received, the unique return path allows the mail system to automatically identify which recipient bounced without parsing the bounce message body. Without proper VERP configuration, bounce handling requires expensive and unreliable bounce message parsing.
Symptoms
- Bounce messages go to a generic catch-all address instead of being processed automatically
- Mailing list software cannot identify which subscriber bounced
- Manual bounce processing required for each failed delivery
- High bounce rate not reflected in subscriber status
- Bounce processing script receives messages it cannot parse:
`- Unknown bounce format - cannot extract recipient address
`
Common Causes
- MTA not configured to use VERP for outgoing mail
- Envelope sender (Return-Path) not set uniquely per recipient
- Postfix VERP feature not enabled
- Bounce processing script expects VERP but MTA sends standard envelope
- Catch-all mailbox consuming all bounces without classification
Step-by-Step Fix
- 1.Understand VERP address format:
- 2.
` - 3.# Standard envelope sender:
- 4.Return-Path: <bounces@example.com>
# VERP envelope sender (encodes recipient): Return-Path: <bounces-user=recipient.com@example.com> ```
- 1.Enable VERP in Postfix:
- 2.
` - 3.# /etc/postfix/main.cf
- 4.# Enable VERP for specific mailing list
- 5.verp_delimiter_filter = -=+
- 6.
` - 7.Then when sending, use the
-Vflag: - 8.```bash
- 9.sendmail -V -f "bounces@example.com" "user@recipient.com"
- 10.
` - 11.Send mail with VERP using Postfix:
- 12.```bash
- 13.# Using postdrop with VERP
- 14.echo -e "Subject: Test\n\nBody" | \
- 15.sendmail -V -f "newsletter-bounces+john=gmail.com@example.com" john@gmail.com
- 16.
` - 17.Process VERP bounces with a script:
- 18.```python
- 19.import re
- 20.import email
def parse_verp_bounce(msg_bytes): msg = email.message_from_bytes(msg_bytes) return_path = msg.get('Return-Path', '')
# Parse VERP address: bounces+recipient=domain.com@example.com match = re.match(r'<bounces\+(.+)=(.+)@example\.com>', return_path) if match: local_part = match.group(1) domain = match.group(2) recipient_email = f"{local_part}@{domain}" return recipient_email
# Also check envelope-to from Received headers for header in msg.get_all('Received', []): match = re.search(r'for\s+<?(.+?)>?\s*;', header) if match: return match.group(1)
return None
# Example usage with open('/var/mail/bounce-handler', 'rb') as f: original_recipient = parse_verp_bounce(f.read()) print(f"Bounced recipient: {original_recipient}") ```
- 1.Configure Postfix to route VERP bounces to a processor:
- 2.
` - 3.# /etc/postfix/main.cf
- 4.# Route all bounces to a local processor
- 5.luser_relay = bounce-handler@localhost
# /etc/aliases bounce-handler: "|/usr/local/bin/process-bounce.py" ```
- 1.For PHPMailer (common application scenario):
- 2.```php
- 3.$mail = new PHPMailer(true);
- 4.$mail->isSMTP();
- 5.$mail->Host = 'smtp.example.com';
- 6.$mail->SMTPAuth = true;
// Enable VERP $mail->Sender = "bounces+{$recipientEmail}=@example.com"; $mail->addCustomHeader('Return-Path', $mail->Sender);
$mail->setFrom('newsletter@example.com', 'Newsletter'); $mail->addAddress($recipientEmail); $mail->send(); ```
- 1.For Amazon SES bounce handling:
- 2.```python
- 3.import boto3
- 4.import json
sns = boto3.client('sns')
def handle_sns_bounce(message): notification = json.loads(message) bounce_data = notification.get('bounce', {}) bounce_type = bounce_data.get('bounceType') # 'Permanent' or 'Transient' recipients = bounce_data.get('bouncedRecipients', [])
for recipient in recipients: email_addr = recipient.get('emailAddress') action = 'disable' if bounce_type == 'Permanent' else 'retry' print(f"{email_addr}: {action} ({bounce_type})")
if bounce_type == 'Permanent': # Disable subscriber in your database disable_subscriber(email_addr) ```
Prevention
- Always use VERP for mailing lists and transactional email
- Process bounces in real-time using a pipe or webhook
- Classify bounces as hard (permanent) or soft (transient)
- Automatically suppress hard-bounced addresses after first occurrence
- Retry soft bounces up to 3 times before marking as hard bounce
- Monitor bounce rates and alert if they exceed 5% of total sends