Tools: From Zero to Production: Building Postfix + AWS SES in 2 Hours

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

Code Block

Copy

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