Tools: From Zero to Production: Building Postfix + AWS SES in 2 Hours
What You Need Before Starting
Phase 1: AWS SES Setup (15 mins)
Step 1: Verify Your Domain
Step 2: Configure DKIM (5 mins)
Step 3: SPF + DMARC (3 mins)
Step 4: Get SMTP Credentials (2 mins)
Phase 2: Postfix Setup (20 mins)
Install Postfix
Configure SES Credentials
Configure Postfix Main Settings
Create Sender Whitelist
Start Postfix
Phase 3: Event Pipeline (25 mins)
Create IAM Role
Create SQS Queue
Create SNS Topic and Subscribe SQS
Configure SES to Publish Events
Phase 4: Logger Deployment (30 mins)
Install Dependencies
Create Logger Script
Create Systemd Service
Start Logger
Phase 5: Testing (20 mins)
Test 1: Complete Flow
Test 2: Bounce Detection
Test 3: Sender Validation
What You Built
Quick Reference
Common Issues
What's Next? AWS account with SES access EC2 instance (t3a.medium, Amazon Linux 2023) Your domain's DNS access 2 hours of focused time That's it. Everything else, we'll build together. AWS Console → SES → Verified Identities → Create Identity Add the TXT record SES provides to your DNS: Look for "VerificationStatus": "Success" Click your domain → DKIM tab → Edit Enable Easy DKIM → Save Add the 3 CNAME records SES provides to your DNS. Should show "DkimEnabled": true SES Console → SMTP Settings → Create SMTP Credentials Save these immediately (you won't see them again): SSH into your server and let's configure Postfix. Add this line (use YOUR credentials): Replace entire contents with: Add approved senders: Look for status=sent (250 Ok...) This is where we set up bounce/delivery tracking. Wait 10 seconds, then verify: Paste this complete script: Should show Active: active (running) Expected: status=bounced, type=Permanent Expected: Sender address rejected: Access denied In 2 hours, you created: - Postfix SMTP relay with sender validation
- AWS SES integration with DKIM/SPF/DMARC- Real-time tracking for delivery and bounces- Unified logging - both "sent" and "delivered" in one place
- Cost-effective - ~$30/month vs $90+ for SaaS Email stuck in queue? Read Part 3: Operations & Troubleshooting 🔗 If this helped or resonated with you, connect with me on LinkedIn. Let’s learn and grow together. 👉 Stay tuned for more behind-the-scenes write-ups and system design breakdowns. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
Identity type: Domain
Domain: yourdomain.com
Identity type: Domain
Domain: yourdomain.com
Identity type: Domain
Domain: yourdomain.com
Type: TXT
Name: _amazonses.yourdomain.com
Value: [provided by SES]
Type: TXT
Name: _amazonses.yourdomain.com
Value: [provided by SES]
Type: TXT
Name: _amazonses.yourdomain.com
Value: [provided by SES]
aws ses get-identity-verification-attributes \ --identities yourdomain.com \ --region ap-south-1
aws ses get-identity-verification-attributes \ --identities yourdomain.com \ --region ap-south-1
aws ses get-identity-verification-attributes \ --identities yourdomain.com \ --region ap-south-1
aws ses get-identity-dkim-attributes \ --identities yourdomain.com \ --region ap-south-1
aws ses get-identity-dkim-attributes \ --identities yourdomain.com \ --region ap-south-1
aws ses get-identity-dkim-attributes \ --identities yourdomain.com \ --region ap-south-1
Type: TXT
Name: yourdomain.com
Value: "v=spf1 include:amazonses.com ~all"
Type: TXT
Name: yourdomain.com
Value: "v=spf1 include:amazonses.com ~all"
Type: TXT
Name: yourdomain.com
Value: "v=spf1 include:amazonses.com ~all"
Type: TXT Name: _dmarc.yourdomain.com
Value: "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
Type: TXT Name: _dmarc.yourdomain.com
Value: "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
Type: TXT Name: _dmarc.yourdomain.com
Value: "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
Username: AKAWSSAMPLEEXAMPLE
Password: wJalrXUtnuTde/EXAMPLE
Username: AKAWSSAMPLEEXAMPLE
Password: wJalrXUtnuTde/EXAMPLE
Username: AKAWSSAMPLEEXAMPLE
Password: wJalrXUtnuTde/EXAMPLE
sudo yum update -y
sudo yum install -y postfix cyrus-sasl-plain mailx
sudo systemctl enable postfix
sudo yum update -y
sudo yum install -y postfix cyrus-sasl-plain mailx
sudo systemctl enable postfix
sudo yum update -y
sudo yum install -y postfix cyrus-sasl-plain mailx
sudo systemctl enable postfix
sudo vim /etc/postfix/sasl_passwd
sudo vim /etc/postfix/sasl_passwd
sudo vim /etc/postfix/sasl_passwd
[email-smtp.ap-south-1.amazonaws.com]:587 YOUR_USERNAME:YOUR_PASSWORD
[email-smtp.ap-south-1.amazonaws.com]:587 YOUR_USERNAME:YOUR_PASSWORD
[email-smtp.ap-south-1.amazonaws.com]:587 YOUR_USERNAME:YOUR_PASSWORD
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo vim /etc/postfix/main.cf
sudo vim /etc/postfix/main.cf
sudo vim /etc/postfix/main.cf
# Basic Settings
myhostname = mail.yourdomain.com
mydomain = yourdomain.com
myorigin = $mydomain
inet_interfaces = all
mynetworks = 127.0.0.0/8, 10.0.0.0/16
mydestination = # AWS SES Relay
relayhost = [email-smtp.ap-south-1.amazonaws.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd # TLS Security
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt # Sender Validation
smtpd_sender_restrictions = check_sender_access hash:/etc/postfix/allowed_senders, reject smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination # Logging
maillog_file = /var/log/postfix/postfix.log
# Basic Settings
myhostname = mail.yourdomain.com
mydomain = yourdomain.com
myorigin = $mydomain
inet_interfaces = all
mynetworks = 127.0.0.0/8, 10.0.0.0/16
mydestination = # AWS SES Relay
relayhost = [email-smtp.ap-south-1.amazonaws.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd # TLS Security
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt # Sender Validation
smtpd_sender_restrictions = check_sender_access hash:/etc/postfix/allowed_senders, reject smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination # Logging
maillog_file = /var/log/postfix/postfix.log
# Basic Settings
myhostname = mail.yourdomain.com
mydomain = yourdomain.com
myorigin = $mydomain
inet_interfaces = all
mynetworks = 127.0.0.0/8, 10.0.0.0/16
mydestination = # AWS SES Relay
relayhost = [email-smtp.ap-south-1.amazonaws.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd # TLS Security
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt # Sender Validation
smtpd_sender_restrictions = check_sender_access hash:/etc/postfix/allowed_senders, reject smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination # Logging
maillog_file = /var/log/postfix/postfix.log
sudo vim /etc/postfix/allowed_senders
sudo vim /etc/postfix/allowed_senders
sudo vim /etc/postfix/allowed_senders
[email protected] OK
[email protected] OK
@yourdomain.com REJECT Not authorized
[email protected] OK
[email protected] OK
@yourdomain.com REJECT Not authorized
[email protected] OK
[email protected] OK
@yourdomain.com REJECT Not authorized
sudo postmap /etc/postfix/allowed_senders
sudo mkdir -p /var/log/postfix
sudo chown postfix:postfix /var/log/postfix
sudo postmap /etc/postfix/allowed_senders
sudo mkdir -p /var/log/postfix
sudo chown postfix:postfix /var/log/postfix
sudo postmap /etc/postfix/allowed_senders
sudo mkdir -p /var/log/postfix
sudo chown postfix:postfix /var/log/postfix
sudo postfix check # Should output nothing
sudo systemctl start postfix
sudo systemctl status postfix
sudo postfix check # Should output nothing
sudo systemctl start postfix
sudo systemctl status postfix
sudo postfix check # Should output nothing
sudo systemctl start postfix
sudo systemctl status postfix
echo "Test" | mail -s "Test Email" -r [email protected] [email protected]
sudo tail -f /var/log/postfix/postfix.log
echo "Test" | mail -s "Test Email" -r [email protected] [email protected]
sudo tail -f /var/log/postfix/postfix.log
echo "Test" | mail -s "Test Email" -r [email protected] [email protected]
sudo tail -f /var/log/postfix/postfix.log
# Trust policy
cat trust-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole" }]
} # Create role
aws iam create-role \ --role-name PostfixSESLogger \ --assume-role-policy-document file://trust-policy.json # Permissions policy
cat policy.json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueUrl" ], "Resource": "arn:aws:sqs:ap-south-1:*:ses-events-queue" }]
} # Attach policy
aws iam put-role-policy \ --role-name PostfixSESLogger \ --policy-name SESLogging \ --policy-document file:///policy.json # Create instance profile
aws iam create-instance-profile --instance-profile-name PostfixSESLogger
aws iam add-role-to-instance-profile \ --instance-profile-name PostfixSESLogger \ --role-name PostfixSESLogger # Attach to instance
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 associate-iam-instance-profile \ --instance-id $INSTANCE_ID \ --iam-instance-profile Name=PostfixSESLogger
# Trust policy
cat trust-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole" }]
} # Create role
aws iam create-role \ --role-name PostfixSESLogger \ --assume-role-policy-document file://trust-policy.json # Permissions policy
cat policy.json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueUrl" ], "Resource": "arn:aws:sqs:ap-south-1:*:ses-events-queue" }]
} # Attach policy
aws iam put-role-policy \ --role-name PostfixSESLogger \ --policy-name SESLogging \ --policy-document file:///policy.json # Create instance profile
aws iam create-instance-profile --instance-profile-name PostfixSESLogger
aws iam add-role-to-instance-profile \ --instance-profile-name PostfixSESLogger \ --role-name PostfixSESLogger # Attach to instance
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 associate-iam-instance-profile \ --instance-id $INSTANCE_ID \ --iam-instance-profile Name=PostfixSESLogger
# Trust policy
cat trust-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole" }]
} # Create role
aws iam create-role \ --role-name PostfixSESLogger \ --assume-role-policy-document file://trust-policy.json # Permissions policy
cat policy.json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueUrl" ], "Resource": "arn:aws:sqs:ap-south-1:*:ses-events-queue" }]
} # Attach policy
aws iam put-role-policy \ --role-name PostfixSESLogger \ --policy-name SESLogging \ --policy-document file:///policy.json # Create instance profile
aws iam create-instance-profile --instance-profile-name PostfixSESLogger
aws iam add-role-to-instance-profile \ --instance-profile-name PostfixSESLogger \ --role-name PostfixSESLogger # Attach to instance
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
aws ec2 associate-iam-instance-profile \ --instance-id $INSTANCE_ID \ --iam-instance-profile Name=PostfixSESLogger
aws sts get-caller-identity # Should show the role
aws sts get-caller-identity # Should show the role
aws sts get-caller-identity # Should show the role
QUEUE_URL=$(aws sqs create-queue \ --queue-name ses-events-queue \ --region ap-south-1 \ --query 'QueueUrl' \ --output text) QUEUE_ARN=$(aws sqs get-queue-attributes \ --queue-url "$QUEUE_URL" \ --attribute-names QueueArn \ --region ap-south-1 \ --query 'Attributes.QueueArn' \ --output text) echo "Queue URL: $QUEUE_URL"
echo "Queue ARN: $QUEUE_ARN"
QUEUE_URL=$(aws sqs create-queue \ --queue-name ses-events-queue \ --region ap-south-1 \ --query 'QueueUrl' \ --output text) QUEUE_ARN=$(aws sqs get-queue-attributes \ --queue-url "$QUEUE_URL" \ --attribute-names QueueArn \ --region ap-south-1 \ --query 'Attributes.QueueArn' \ --output text) echo "Queue URL: $QUEUE_URL"
echo "Queue ARN: $QUEUE_ARN"
QUEUE_URL=$(aws sqs create-queue \ --queue-name ses-events-queue \ --region ap-south-1 \ --query 'QueueUrl' \ --output text) QUEUE_ARN=$(aws sqs get-queue-attributes \ --queue-url "$QUEUE_URL" \ --attribute-names QueueArn \ --region ap-south-1 \ --query 'Attributes.QueueArn' \ --output text) echo "Queue URL: $QUEUE_URL"
echo "Queue ARN: $QUEUE_ARN"
SNS_ARN=$(aws sns create-topic \ --name ses-events-topic \ --region ap-south-1 \ --query 'TopicArn' \ --output text) # Allow SNS to send to SQS
cat sqs-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "sns.amazonaws.com"}, "Action": "sqs:SendMessage", "Resource": "$QUEUE_ARN", "Condition": {"ArnEquals": {"aws:SourceArn": "$SNS_ARN"}} }]
} aws sqs set-queue-attributes \ --queue-url "$QUEUE_URL" \ --attributes Policy="$(cat /tmp/sqs-policy.json)" \ --region ap-south-1 # Subscribe SQS to SNS
aws sns subscribe \ --topic-arn "$SNS_ARN" \ --protocol sqs \ --notification-endpoint "$QUEUE_ARN" \ --region ap-south-1
SNS_ARN=$(aws sns create-topic \ --name ses-events-topic \ --region ap-south-1 \ --query 'TopicArn' \ --output text) # Allow SNS to send to SQS
cat sqs-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "sns.amazonaws.com"}, "Action": "sqs:SendMessage", "Resource": "$QUEUE_ARN", "Condition": {"ArnEquals": {"aws:SourceArn": "$SNS_ARN"}} }]
} aws sqs set-queue-attributes \ --queue-url "$QUEUE_URL" \ --attributes Policy="$(cat /tmp/sqs-policy.json)" \ --region ap-south-1 # Subscribe SQS to SNS
aws sns subscribe \ --topic-arn "$SNS_ARN" \ --protocol sqs \ --notification-endpoint "$QUEUE_ARN" \ --region ap-south-1
SNS_ARN=$(aws sns create-topic \ --name ses-events-topic \ --region ap-south-1 \ --query 'TopicArn' \ --output text) # Allow SNS to send to SQS
cat sqs-policy.json
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "sns.amazonaws.com"}, "Action": "sqs:SendMessage", "Resource": "$QUEUE_ARN", "Condition": {"ArnEquals": {"aws:SourceArn": "$SNS_ARN"}} }]
} aws sqs set-queue-attributes \ --queue-url "$QUEUE_URL" \ --attributes Policy="$(cat /tmp/sqs-policy.json)" \ --region ap-south-1 # Subscribe SQS to SNS
aws sns subscribe \ --topic-arn "$SNS_ARN" \ --protocol sqs \ --notification-endpoint "$QUEUE_ARN" \ --region ap-south-1
for EVENT in Delivery Bounce Complaint; do aws ses set-identity-notification-topic \ --identity yourdomain.com \ --notification-type $EVENT \ --sns-topic "$SNS_ARN" \ --region ap-south-1
done # Disable email forwarding
aws ses set-identity-feedback-forwarding-enabled \ --identity yourdomain.com \ --no-forwarding-enabled \ --region ap-south-1
for EVENT in Delivery Bounce Complaint; do aws ses set-identity-notification-topic \ --identity yourdomain.com \ --notification-type $EVENT \ --sns-topic "$SNS_ARN" \ --region ap-south-1
done # Disable email forwarding
aws ses set-identity-feedback-forwarding-enabled \ --identity yourdomain.com \ --no-forwarding-enabled \ --region ap-south-1
for EVENT in Delivery Bounce Complaint; do aws ses set-identity-notification-topic \ --identity yourdomain.com \ --notification-type $EVENT \ --sns-topic "$SNS_ARN" \ --region ap-south-1
done # Disable email forwarding
aws ses set-identity-feedback-forwarding-enabled \ --identity yourdomain.com \ --no-forwarding-enabled \ --region ap-south-1
sudo yum install -y python3-boto3
python3 -c "import boto3; print('✓ boto3 installed')"
sudo yum install -y python3-boto3
python3 -c "import boto3; print('✓ boto3 installed')"
sudo yum install -y python3-boto3
python3 -c "import boto3; print('✓ boto3 installed')"
sudo nano /usr/local/bin/ses_logger.py
sudo nano /usr/local/bin/ses_logger.py
sudo nano /usr/local/bin/ses_logger.py
#!/usr/bin/env python3
import boto3, json, syslog, os, sys
from datetime import datetime REGION = 'ap-south-1'
syslog.openlog('postfix/ses-events', logoption=syslog.LOG_PID, facility=syslog.LOG_MAIL) def log_event(msg_id, event_type, recipient, details): log = f"{msg_id}: to=<{recipient}>, relay=amazonses.com, {details}" level = syslog.LOG_WARNING if event_type == "Bounce" else syslog.LOG_INFO syslog.syslog(level, log) print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {event_type}: {log}") def process_event(message): try: event = json.loads(message) event_type = event.get('notificationType') mail = event.get('mail', {}) msg_id = mail.get('messageId', 'UNKNOWN') if event_type == 'Delivery': delivery = event.get('delivery', {}) for recipient in delivery.get('recipients', []): delay = delivery.get('processingTimeMillis', 0) details = f"dsn=2.0.0, status=delivered, delay={delay}ms" log_event(msg_id, event_type, recipient, details) elif event_type == 'Bounce': bounce = event.get('bounce', {}) for r in bounce.get('bouncedRecipients', []): details = f"dsn=5.0.0, status=bounced, type={bounce.get('bounceType')}" log_event(msg_id, event_type, r.get('emailAddress'), details) return True except Exception as e: print(f"Error: {e}", file=sys.stderr) return False def main(): queue_url = os.environ.get('SQS_QUEUE_URL') if not queue_url: sys.exit("ERROR: SQS_QUEUE_URL not set") print(f"SES Logger Started\nQueue: {queue_url}\n") sqs = boto3.client('sqs', region_name=REGION) while True: try: response = sqs.receive_message( QueueUrl=queue_url, MaxNumberOfMessages=10, WaitTimeSeconds=20 ) for message in response.get('Messages', []): body = json.loads(message['Body']) if process_event(body.get('Message')): sqs.delete_message( QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'] ) except KeyboardInterrupt: break except Exception as e: print(f"Error: {e}", file=sys.stderr) if __name__ == '__main__': main()
#!/usr/bin/env python3
import boto3, json, syslog, os, sys
from datetime import datetime REGION = 'ap-south-1'
syslog.openlog('postfix/ses-events', logoption=syslog.LOG_PID, facility=syslog.LOG_MAIL) def log_event(msg_id, event_type, recipient, details): log = f"{msg_id}: to=<{recipient}>, relay=amazonses.com, {details}" level = syslog.LOG_WARNING if event_type == "Bounce" else syslog.LOG_INFO syslog.syslog(level, log) print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {event_type}: {log}") def process_event(message): try: event = json.loads(message) event_type = event.get('notificationType') mail = event.get('mail', {}) msg_id = mail.get('messageId', 'UNKNOWN') if event_type == 'Delivery': delivery = event.get('delivery', {}) for recipient in delivery.get('recipients', []): delay = delivery.get('processingTimeMillis', 0) details = f"dsn=2.0.0, status=delivered, delay={delay}ms" log_event(msg_id, event_type, recipient, details) elif event_type == 'Bounce': bounce = event.get('bounce', {}) for r in bounce.get('bouncedRecipients', []): details = f"dsn=5.0.0, status=bounced, type={bounce.get('bounceType')}" log_event(msg_id, event_type, r.get('emailAddress'), details) return True except Exception as e: print(f"Error: {e}", file=sys.stderr) return False def main(): queue_url = os.environ.get('SQS_QUEUE_URL') if not queue_url: sys.exit("ERROR: SQS_QUEUE_URL not set") print(f"SES Logger Started\nQueue: {queue_url}\n") sqs = boto3.client('sqs', region_name=REGION) while True: try: response = sqs.receive_message( QueueUrl=queue_url, MaxNumberOfMessages=10, WaitTimeSeconds=20 ) for message in response.get('Messages', []): body = json.loads(message['Body']) if process_event(body.get('Message')): sqs.delete_message( QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'] ) except KeyboardInterrupt: break except Exception as e: print(f"Error: {e}", file=sys.stderr) if __name__ == '__main__': main()
#!/usr/bin/env python3
import boto3, json, syslog, os, sys
from datetime import datetime REGION = 'ap-south-1'
syslog.openlog('postfix/ses-events', logoption=syslog.LOG_PID, facility=syslog.LOG_MAIL) def log_event(msg_id, event_type, recipient, details): log = f"{msg_id}: to=<{recipient}>, relay=amazonses.com, {details}" level = syslog.LOG_WARNING if event_type == "Bounce" else syslog.LOG_INFO syslog.syslog(level, log) print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] {event_type}: {log}") def process_event(message): try: event = json.loads(message) event_type = event.get('notificationType') mail = event.get('mail', {}) msg_id = mail.get('messageId', 'UNKNOWN') if event_type == 'Delivery': delivery = event.get('delivery', {}) for recipient in delivery.get('recipients', []): delay = delivery.get('processingTimeMillis', 0) details = f"dsn=2.0.0, status=delivered, delay={delay}ms" log_event(msg_id, event_type, recipient, details) elif event_type == 'Bounce': bounce = event.get('bounce', {}) for r in bounce.get('bouncedRecipients', []): details = f"dsn=5.0.0, status=bounced, type={bounce.get('bounceType')}" log_event(msg_id, event_type, r.get('emailAddress'), details) return True except Exception as e: print(f"Error: {e}", file=sys.stderr) return False def main(): queue_url = os.environ.get('SQS_QUEUE_URL') if not queue_url: sys.exit("ERROR: SQS_QUEUE_URL not set") print(f"SES Logger Started\nQueue: {queue_url}\n") sqs = boto3.client('sqs', region_name=REGION) while True: try: response = sqs.receive_message( QueueUrl=queue_url, MaxNumberOfMessages=10, WaitTimeSeconds=20 ) for message in response.get('Messages', []): body = json.loads(message['Body']) if process_event(body.get('Message')): sqs.delete_message( QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'] ) except KeyboardInterrupt: break except Exception as e: print(f"Error: {e}", file=sys.stderr) if __name__ == '__main__': main()
sudo chmod +x /usr/local/bin/ses_logger.py
sudo chmod +x /usr/local/bin/ses_logger.py
sudo chmod +x /usr/local/bin/ses_logger.py
QUEUE_URL=$(aws sqs get-queue-url --queue-name ses-events-queue --region ap-south-1 --query 'QueueUrl' --output text) sudo vim /etc/systemd/system/ses-logger.service
[Unit]
Description=SES Event Logger
After=network.target [Service]
Type=simple
User=root
Environment="SQS_QUEUE_URL=$QUEUE_URL"
Environment="AWS_DEFAULT_REGION=ap-south-1"
ExecStart=/usr/bin/python3 /usr/local/bin/ses_logger.py
Restart=always
RestartSec=10
StandardOutput=append:/var/log/postfix/ses-logger.log
StandardError=append:/var/log/postfix/ses-logger-error.log [Install]
WantedBy=multi-user.target
QUEUE_URL=$(aws sqs get-queue-url --queue-name ses-events-queue --region ap-south-1 --query 'QueueUrl' --output text) sudo vim /etc/systemd/system/ses-logger.service
[Unit]
Description=SES Event Logger
After=network.target [Service]
Type=simple
User=root
Environment="SQS_QUEUE_URL=$QUEUE_URL"
Environment="AWS_DEFAULT_REGION=ap-south-1"
ExecStart=/usr/bin/python3 /usr/local/bin/ses_logger.py
Restart=always
RestartSec=10
StandardOutput=append:/var/log/postfix/ses-logger.log
StandardError=append:/var/log/postfix/ses-logger-error.log [Install]
WantedBy=multi-user.target
QUEUE_URL=$(aws sqs get-queue-url --queue-name ses-events-queue --region ap-south-1 --query 'QueueUrl' --output text) sudo vim /etc/systemd/system/ses-logger.service
[Unit]
Description=SES Event Logger
After=network.target [Service]
Type=simple
User=root
Environment="SQS_QUEUE_URL=$QUEUE_URL"
Environment="AWS_DEFAULT_REGION=ap-south-1"
ExecStart=/usr/bin/python3 /usr/local/bin/ses_logger.py
Restart=always
RestartSec=10
StandardOutput=append:/var/log/postfix/ses-logger.log
StandardError=append:/var/log/postfix/ses-logger-error.log [Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable ses-logger
sudo systemctl start ses-logger
sudo systemctl status ses-logger
sudo systemctl daemon-reload
sudo systemctl enable ses-logger
sudo systemctl start ses-logger
sudo systemctl status ses-logger
sudo systemctl daemon-reload
sudo systemctl enable ses-logger
sudo systemctl start ses-logger
sudo systemctl status ses-logger
sudo tail -f /var/log/postfix/ses-logger.log
sudo tail -f /var/log/postfix/ses-logger.log
sudo tail -f /var/log/postfix/ses-logger.log
echo "Test from infrastructure" | \ mail -s "Test Email" -r [email protected] [email protected]
echo "Test from infrastructure" | \ mail -s "Test Email" -r [email protected] [email protected]
echo "Test from infrastructure" | \ mail -s "Test Email" -r [email protected] [email protected]
# Terminal 1 - Sent status (immediate)
sudo tail -f /var/log/postfix/postfix.log | grep "status=sent" # Terminal 2 - Delivered status (10-30 sec delay)
sudo tail -f /var/log/postfix/mail.log | grep "status=delivered"
# Terminal 1 - Sent status (immediate)
sudo tail -f /var/log/postfix/postfix.log | grep "status=sent" # Terminal 2 - Delivered status (10-30 sec delay)
sudo tail -f /var/log/postfix/mail.log | grep "status=delivered"
# Terminal 1 - Sent status (immediate)
sudo tail -f /var/log/postfix/postfix.log | grep "status=sent" # Terminal 2 - Delivered status (10-30 sec delay)
sudo tail -f /var/log/postfix/mail.log | grep "status=delivered"
# Postfix log (immediate):
status=sent (250 Ok 0109019c...) # Mail log (after 10-30 seconds):
status=delivered, delay=3558ms
# Postfix log (immediate):
status=sent (250 Ok 0109019c...) # Mail log (after 10-30 seconds):
status=delivered, delay=3558ms
# Postfix log (immediate):
status=sent (250 Ok 0109019c...) # Mail log (after 10-30 seconds):
status=delivered, delay=3558ms
# Use SES bounce simulator
echo "Bounce test" | \ mail -s "Bounce Test" -r [email protected] [email protected] # Watch for bounce (1-2 mins)
sudo tail -f /var/log/postfix/mail.log | grep "bounced"
# Use SES bounce simulator
echo "Bounce test" | \ mail -s "Bounce Test" -r [email protected] [email protected] # Watch for bounce (1-2 mins)
sudo tail -f /var/log/postfix/mail.log | grep "bounced"
# Use SES bounce simulator
echo "Bounce test" | \ mail -s "Bounce Test" -r [email protected] [email protected] # Watch for bounce (1-2 mins)
sudo tail -f /var/log/postfix/mail.log | grep "bounced"
# Try unauthorized sender
echo "Should fail" | \ mail -s "Test" -r [email protected] [email protected] # Check rejection
sudo tail /var/log/postfix/postfix.log | grep reject
# Try unauthorized sender
echo "Should fail" | \ mail -s "Test" -r [email protected] [email protected] # Check rejection
sudo tail /var/log/postfix/postfix.log | grep reject
# Try unauthorized sender
echo "Should fail" | \ mail -s "Test" -r [email protected] [email protected] # Check rejection
sudo tail /var/log/postfix/postfix.log | grep reject
sudo systemctl restart postfix
sudo systemctl restart ses-logger
sudo systemctl restart postfix
sudo systemctl restart ses-logger
sudo systemctl restart postfix
sudo systemctl restart ses-logger
sudo tail -f /var/log/postfix/postfix.log # Sent
sudo tail -f /var/log/postfix/mail.log # Delivered
sudo tail -f /var/log/postfix/postfix.log # Sent
sudo tail -f /var/log/postfix/mail.log # Delivered
sudo tail -f /var/log/postfix/postfix.log # Sent
sudo tail -f /var/log/postfix/mail.log # Delivered
grep "[email protected]" /var/log/postfix/*.log
grep "[email protected]" /var/log/postfix/*.log
grep "[email protected]" /var/log/postfix/*.log
# Check why
sudo tail -50 /var/log/postfix/postfix.log | grep deferred
# Flush after fixing
sudo postqueue -f
# Check why
sudo tail -50 /var/log/postfix/postfix.log | grep deferred
# Flush after fixing
sudo postqueue -f
# Check why
sudo tail -50 /var/log/postfix/postfix.log | grep deferred
# Flush after fixing
sudo postqueue -f
# Check errors
sudo journalctl -u ses-logger -n 50
# Restart
sudo systemctl restart ses-logger
# Check errors
sudo journalctl -u ses-logger -n 50
# Restart
sudo systemctl restart ses-logger
# Check errors
sudo journalctl -u ses-logger -n 50
# Restart
sudo systemctl restart ses-logger
# Check SQS has messages
aws sqs get-queue-attributes \ --queue-url "YOUR_QUEUE_URL" \ --attribute-names ApproximateNumberOfMessages
# Check SQS has messages
aws sqs get-queue-attributes \ --queue-url "YOUR_QUEUE_URL" \ --attribute-names ApproximateNumberOfMessages
# Check SQS has messages
aws sqs get-queue-attributes \ --queue-url "YOUR_QUEUE_URL" \ --attribute-names ApproximateNumberOfMessages - AWS account with SES access
- EC2 instance (t3a.medium, Amazon Linux 2023)
- Your domain's DNS access
- 2 hours of focused time - Click your domain → DKIM tab → Edit
- Enable Easy DKIM → Save