212 lines
7.4 KiB
Markdown
212 lines
7.4 KiB
Markdown
# Email-Based Bounce Handling Setup (Comprehensive)
|
|
|
|
This document explains the complete email-based bounce handling system implemented as an alternative to SNS webhooks for environments without SES production access.
|
|
|
|
## Overview
|
|
|
|
The system processes email bounces directly within the Postfix container by:
|
|
1. Rewriting return paths to direct bounces to a processing address
|
|
2. Processing bounce emails via Python script
|
|
3. Updating member bounce statistics in MySQL database
|
|
4. Automatically disabling members with excessive bounces
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables (in .env)
|
|
|
|
```bash
|
|
# Bounce handling feature flags
|
|
ENABLE_BOUNCE_HANDLING=true # Master switch for bounce functionality
|
|
ENABLE_EMAIL_BOUNCE_PROCESSING=true # Enable email-based processing
|
|
ENABLE_SNS_WEBHOOKS=false # Disable SNS webhooks (optional)
|
|
|
|
# Database settings (required for bounce processing)
|
|
MYSQL_ROOT_PASSWORD=your_root_password
|
|
MYSQL_DATABASE=maillist
|
|
MYSQL_USER=maillist
|
|
MYSQL_PASSWORD=your_maillist_password
|
|
```
|
|
|
|
### Key Components
|
|
|
|
#### 1. Return Path Rewriting (`postfix/smtp_generic`)
|
|
Routes all bounces to the processing address:
|
|
```
|
|
@lists.sasalliance.org bounces@lists.sasalliance.org
|
|
@sasalliance.org bounces@lists.sasalliance.org
|
|
```
|
|
|
|
#### 2. Bounce Processing Script (`postfix/process-bounce.py`)
|
|
Python script that:
|
|
- Parses bounce emails for recipient addresses and bounce types
|
|
- Updates bounce counts in MySQL database
|
|
- Automatically disables members after 5 hard bounces
|
|
- Logs all bounce events
|
|
|
|
#### 3. Postfix Integration (`postfix/main.cf.template`)
|
|
```
|
|
# Return path rewriting for outbound mail
|
|
smtp_generic_maps = hash:/etc/postfix/smtp_generic
|
|
|
|
# Bounce processing
|
|
bounce_notice_recipient = bounces@lists.sasalliance.org
|
|
```
|
|
|
|
#### 4. Email Aliases (`postfix/entrypoint.sh`)
|
|
```
|
|
# Route bounces to processing script
|
|
bounces: "|/usr/local/bin/python3 /etc/postfix/process-bounce.py"
|
|
```
|
|
|
|
## How It Works
|
|
|
|
### Outbound Email Flow
|
|
1. User sends email to `community@lists.sasalliance.org`
|
|
2. Postfix expands to member list via MySQL
|
|
3. Email sent via SES with return path rewritten to `bounces@lists.sasalliance.org`
|
|
4. If delivery fails, bounce goes to bounce processing address
|
|
|
|
### Bounce Processing Flow
|
|
1. Bounce email arrives at `bounces@lists.sasalliance.org`
|
|
2. Postfix pipes email to `process-bounce.py` script
|
|
3. Script parses bounce for recipient and bounce type
|
|
4. Database updated with bounce information
|
|
5. Member automatically disabled if hard bounce threshold reached
|
|
|
|
### Bounce Types Detected
|
|
- **Hard Bounces**: Permanent failures (5.x.x SMTP codes)
|
|
- Invalid email addresses
|
|
- Domain doesn't exist
|
|
- Mailbox doesn't exist
|
|
- **Soft Bounces**: Temporary failures (4.x.x SMTP codes)
|
|
- Mailbox full
|
|
- Temporary server issues
|
|
- Rate limiting
|
|
|
|
## Database Schema
|
|
|
|
### Bounce Logs Table
|
|
```sql
|
|
CREATE TABLE bounce_logs (
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
email VARCHAR(255) NOT NULL,
|
|
bounce_type ENUM('hard', 'soft', 'complaint') NOT NULL,
|
|
bounce_reason TEXT,
|
|
bounced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
raw_message TEXT,
|
|
INDEX idx_email (email),
|
|
INDEX idx_bounced_at (bounced_at)
|
|
);
|
|
```
|
|
|
|
### Members Table (bounce tracking fields)
|
|
```sql
|
|
ALTER TABLE members ADD COLUMN bounce_count INT DEFAULT 0;
|
|
ALTER TABLE members ADD COLUMN last_bounce_at TIMESTAMP NULL;
|
|
ALTER TABLE members ADD COLUMN active BOOLEAN DEFAULT true;
|
|
```
|
|
|
|
## Testing Bounce Handling
|
|
|
|
### 1. Test Return Path Configuration
|
|
```bash
|
|
# Check that return path rewriting is working
|
|
sudo docker-compose exec postfix postconf smtp_generic_maps
|
|
sudo docker-compose exec postfix postmap -q "test@lists.sasalliance.org" hash:/etc/postfix/smtp_generic
|
|
```
|
|
|
|
### 2. Simulate Bounce Email
|
|
```bash
|
|
# Send test bounce to processing script
|
|
echo "Subject: Delivery Status Notification (Failure)
|
|
From: MAILER-DAEMON@ses.amazonaws.com
|
|
To: bounces@lists.sasalliance.org
|
|
|
|
The following message could not be delivered:
|
|
Recipient: test@example.com
|
|
Reason: 550 5.1.1 User unknown" | docker-compose exec -T postfix mail -s "Test Bounce" bounces@lists.sasalliance.org
|
|
```
|
|
|
|
### 3. Check Bounce Processing
|
|
```bash
|
|
# View bounce logs
|
|
sudo docker-compose exec mysql mysql -u maillist -pmaillist maillist -e "SELECT * FROM bounce_logs ORDER BY bounced_at DESC LIMIT 5;"
|
|
|
|
# Check member bounce counts
|
|
sudo docker-compose exec mysql mysql -u maillist -pmaillist maillist -e "SELECT email, bounce_count, last_bounce_at, active FROM members WHERE bounce_count > 0;"
|
|
```
|
|
|
|
## Monitoring and Maintenance
|
|
|
|
### View Processing Logs
|
|
```bash
|
|
# Monitor bounce processing
|
|
sudo docker-compose logs -f postfix | grep -E "(bounce|process-bounce)"
|
|
|
|
# Check API bounce handling status
|
|
curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8000/config
|
|
```
|
|
|
|
### Reset Member Bounce Count
|
|
```bash
|
|
# Via API
|
|
curl -X POST http://localhost:8000/members/{member_id}/reset-bounces \
|
|
-H "Authorization: Bearer $API_TOKEN"
|
|
|
|
# Via Database
|
|
sudo docker-compose exec mysql mysql -u maillist -pmaillist maillist -e "UPDATE members SET bounce_count=0, active=true WHERE email='user@example.com';"
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Issues
|
|
|
|
#### Bounces Not Being Processed
|
|
1. Check that `bounces` alias exists: `sudo docker-compose exec postfix cat /etc/aliases | grep bounces`
|
|
2. Verify Python script permissions: `sudo docker-compose exec postfix ls -la /etc/postfix/process-bounce.py`
|
|
3. Test script manually: `sudo docker-compose exec postfix python3 /etc/postfix/process-bounce.py --test`
|
|
|
|
#### Return Path Not Rewritten
|
|
1. Check smtp_generic configuration: `sudo docker-compose exec postfix postconf smtp_generic_maps`
|
|
2. Verify map file exists: `sudo docker-compose exec postfix ls -la /etc/postfix/smtp_generic*`
|
|
3. Test mapping: `sudo docker-compose exec postfix postmap -q "test@lists.sasalliance.org" hash:/etc/postfix/smtp_generic`
|
|
|
|
#### Database Connection Issues
|
|
1. Check PyMySQL installation: `sudo docker-compose exec postfix python3 -c "import pymysql; print('OK')"`
|
|
2. Test database connection: `sudo docker-compose exec postfix python3 -c "import pymysql; pymysql.connect(host='mysql', user='maillist', password='your_password', database='maillist')"`
|
|
3. Verify network connectivity: `sudo docker-compose exec postfix ping mysql`
|
|
|
|
### Log Analysis
|
|
```bash
|
|
# Postfix logs
|
|
sudo docker-compose logs postfix | grep -E "(bounce|MAILER-DAEMON|process-bounce)"
|
|
|
|
# MySQL connection logs
|
|
sudo docker-compose logs postfix | grep -E "(pymysql|mysql)"
|
|
|
|
# SES relay logs
|
|
sudo docker-compose logs postfix | grep -E "(relay|sent|deferred)"
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
- Bounce processing script runs with limited privileges
|
|
- Database credentials secured in environment variables
|
|
- Bounce emails contain sensitive delivery information - logs are rotated
|
|
- Return path rewriting prevents bounce loops
|
|
- Processing script validates email format before database updates
|
|
|
|
## Performance Notes
|
|
|
|
- Bounce processing is asynchronous (doesn't block email delivery)
|
|
- Database queries are indexed for bounce lookups
|
|
- Bounce logs should be periodically archived for large volumes
|
|
- SMTP generic maps are cached by Postfix for performance
|
|
|
|
## Advantages over SNS Webhooks
|
|
|
|
- **Works with SES Sandbox**: No production SES access required
|
|
- **No External Dependencies**: Doesn't require SNS, webhooks, or HTTPS domains
|
|
- **Self-Contained**: All processing happens within existing containers
|
|
- **Real-time Processing**: Bounces processed as emails arrive
|
|
- **Compatible**: Uses same database schema and UI as SNS method |