When managing email newsletters, handling bounce messages is crucial for maintaining list hygiene. The standard approach involves using unique return-path addresses like bounce-123456789@example.com
to track delivery failures. In Postfix, we can implement this through several configuration steps.
To capture all bounce messages in a single mailbox, we'll use Postfix's virtual aliases. Here's the complete setup:
# /etc/postfix/virtual bounce-*@example.com bouncebox
Then update the main Postfix configuration:
# /etc/postfix/main.cf virtual_alias_maps = hash:/etc/postfix/virtual
After making these changes, run:
postmap /etc/postfix/virtual postfix reload
When sending newsletters, you should set the Return-Path header to your bounce tracking address. Here's an example in PHP:
$headers = [ 'Return-Path: bounce-'.time().'@example.com', 'From: newsletter@example.com', 'Content-Type: text/html; charset=UTF-8' ]; mail($to, $subject, $message, implode("\r\n", $headers));
There are two main types of bounces:
- Hard bounces: Permanent delivery failures (invalid addresses, domain doesn't exist)
- Soft bounces: Temporary issues (mailbox full, server down)
Here's a Python script example to parse bounce messages:
import re import email def parse_bounce(message): msg = email.message_from_string(message) subject = msg['subject'] body = msg.get_payload() # Check for common bounce patterns if '550' in subject or 'permanent failure' in body.lower(): return 'hard' elif '421' in subject or 'temporary failure' in body.lower(): return 'soft' return None
Follow these rules for bounce handling:
- Remove addresses after 1 hard bounce
- Remove addresses after 3 soft bounces within 30 days
- Implement a quarantine period for soft bounces
Variable Envelope Return Path (VERP) provides more granular tracking. Here's how to implement it in Postfix:
# /etc/postfix/main.cf recipient_delimiter = +
Then use addresses like newsletter+user=domain.com@example.com
where the original recipient is encoded in the local part.
When sending newsletters through Postfix, bounce management is critical for maintaining list hygiene. The common pattern involves:
Return-Path: bounce-123456789@example.com
From: newsletter@yourdomain.com
To: subscriber@gmail.com
To capture all bounce-* prefixed emails into a single mailbox, add these configurations to main.cf
:
# Enable recipient address rewriting
recipient_canonical_maps = regexp:/etc/postfix/recipient_canonical
# Virtual mailbox configuration
virtual_alias_maps = regexp:/etc/postfix/virtual_aliases
virtual_mailbox_base = /var/mail/vhosts
virtual_mailbox_domains = example.com
virtual_mailbox_maps = hash:/etc/postfix/virtual_mailboxes
Create /etc/postfix/recipient_canonical
with:
/^bounce-.*@example\.com$/ bounce-catchall@example.com
Here's a Python script to parse Postfix bounce messages:
import email
import re
from mailbox import mbox
def parse_bounces(mbox_path):
pattern = re.compile(r'bounce-(\d+)@example\.com')
bounces = {'hard': set(), 'soft': set()}
for message in mbox(mbox_path):
if message['X-Failed-Recipients']:
original_id = pattern.search(message['Return-Path']).group(1)
if '5.1.1' in message.get_payload(): # Hard bounce code
bounces['hard'].add(original_id)
else: # Soft bounce
bounces['soft'].add(original_id)
return bounces
Key SMTP codes to identify:
- Hard bounces (permanent failures): 5.1.1, 5.1.2, 5.4.1
- Soft bounces (temporary issues): 4.2.2, 4.3.1, 4.4.7
Recommended handling strategy:
def manage_subscription(bounce_data):
for sub_id in bounce_data['hard']:
db.execute("UPDATE subscribers SET status='bounced' WHERE id=?", (sub_id,))
for sub_id in bounce_data['soft']:
soft_count = db.execute("SELECT soft_bounces FROM subscribers WHERE id=?", (sub_id,))
if soft_count >= 3:
db.execute("UPDATE subscribers SET status='bounced' WHERE id=?", (sub_id,))
For better bounce classification with SpamAssassin:
content_filter = smtp-amavis:[127.0.0.1]:10024
# In amavisd.conf:
$bounce_killer_score = 6.3;
$sa_tag_level_deflt = -999;
$sa_tag2_level_deflt = 4.0;