SES Email Notification Implementation Guide ¶
Complete step-by-step guide to implement SES bounce and complaint notifications via email.
Table of Contents ¶
- Prerequisites
- Step 1: Verify Email Addresses in SES
- Step 2: Create SNS Topic
- Step 3: Create SES Configuration Set
- Step 4: Create IAM Role for Lambda
- Step 5: Create Lambda Function
- Step 6: Subscribe Lambda to SNS Topic
- Step 7: Test the Implementation
- Step 8: Apply Configuration Set to Your Emails
- Troubleshooting
Prerequisites ¶
- AWS Account with appropriate permissions
- AWS CLI installed and configured (optional but recommended)
- At least one verified email address or domain in SES
Step 1: Verify Email Addresses in SES ¶
Via AWS Console: ¶
- Navigate to Amazon SES console
- Click Verified identities in the left menu
- Click Create identity
- Choose Email address
- Enter the email address you want to use as the sender (this will send notifications)
- Click Create identity
- Check your email inbox and click the verification link
- Repeat for the recipient email address if different
Via AWS CLI: ¶
# Verify sender email
aws ses verify-email-identity --email-address sender@yourdomain.com
# Verify recipient email
aws ses verify-email-identity --email-address recipient@yourdomain.com
Note: Both sender and recipient must be verified if your SES account is in sandbox mode. For production, you can request to move out of sandbox.
Step 2: Create SNS Topic ¶
Via AWS Console: ¶
- Navigate to Amazon SNS console
- Click Topics in the left menu
- Click Create topic
- Choose Standard type
- Enter topic name:
ses-bounce-complaint-notifications - Click Create topic
- Copy the Topic ARN - you'll need this later
Via AWS CLI: ¶
# Create SNS topic
aws sns create-topic --name ses-bounce-complaint-notifications
# Note the TopicArn from the response
Example ARN: arn:aws:sns:us-east-1:123456789012:ses-bounce-complaint-notifications
Step 3: Create SES Configuration Set ¶
Via AWS Console: ¶
- Navigate to Amazon SES console
- Click Configuration sets in the left menu
- Click Create set
- Enter name:
email-tracking-config-set - Click Create set
Add Event Destination for Bounces: ¶
- Click on your newly created configuration set
- Go to Event destinations tab
- Click Add destination
- Select Amazon SNS as destination type
- Click Next
- Configure:
- Name:
bounce-notifications - Event types: Check Bounce
- Amazon SNS topic: Select your SNS topic created in Step 2
- Name:
- Click Next and then Add destination
Add Event Destination for Complaints: ¶
- Click Add destination again
- Select Amazon SNS as destination type
- Click Next
- Configure:
- Name:
complaint-notifications - Event types: Check Complaint
- Amazon SNS topic: Select the same SNS topic
- Name:
- Click Next and then Add destination
Via AWS CLI: ¶
# Create configuration set
aws sesv2 create-configuration-set \
--configuration-set-name email-tracking-config-set
# Add bounce event destination
aws sesv2 create-configuration-set-event-destination \
--configuration-set-name email-tracking-config-set \
--event-destination-name bounce-notifications \
--event-destination '{
"Enabled": true,
"MatchingEventTypes": ["BOUNCE"],
"SnsDestination": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:ses-bounce-complaint-notifications"
}
}'
# Add complaint event destination
aws sesv2 create-configuration-set-event-destination \
--configuration-set-name email-tracking-config-set \
--event-destination-name complaint-notifications \
--event-destination '{
"Enabled": true,
"MatchingEventTypes": ["COMPLAINT"],
"SnsDestination": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:ses-bounce-complaint-notifications"
}
}'
Step 4: Create IAM Role for Lambda ¶
Via AWS Console: ¶
- Navigate to IAM console
- Click Roles in the left menu
- Click Create role
- Select AWS service and choose Lambda
- Click Next
- Attach these managed policies:
AWSLambdaBasicExecutionRole(for CloudWatch Logs)
- Click Next
- Enter role name:
SESNotificationLambdaRole - Click Create role
- Click on the role you just created
- Click Add permissions → Create inline policy
- Switch to JSON tab and paste:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ses:SendEmail",
"Resource": "*"
}
]
}
- Click Review policy
- Name it:
SESSendEmailPolicy - Click Create policy
Via AWS CLI: ¶
# Create trust policy file
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# Create role
aws iam create-role \
--role-name SESNotificationLambdaRole \
--assume-role-policy-document file://trust-policy.json
# Attach basic execution policy
aws iam attach-role-policy \
--role-name SESNotificationLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Create SES send email policy
cat > ses-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ses:SendEmail",
"Resource": "*"
}
]
}
EOF
# Create and attach inline policy
aws iam put-role-policy \
--role-name SESNotificationLambdaRole \
--policy-name SESSendEmailPolicy \
--policy-document file://ses-policy.json
Step 5: Create Lambda Function ¶
Via AWS Console: ¶
- Navigate to AWS Lambda console
- Click Create function
- Choose Author from scratch
- Configure:
- Function name:
SESNotificationHandler - Runtime: Python 3.12 (or latest Python version)
- Architecture: x86_64
- Execution role: Use an existing role →
SESNotificationLambdaRole
- Function name:
- Click Create function
Upload Function Code: ¶
- In the Code tab, delete the default code
- Copy and paste the contents of
lambda_function.pyfrom this repository - Click Deploy
Configure Environment Variables: ¶
- Go to Configuration tab
- Click Environment variables
- Click Edit
- Add these variables:
- Key:
SENDER_EMAIL| Value:sender@yourdomain.com(verified in Step 1) - Key:
RECIPIENT_EMAIL| Value:recipient@yourdomain.com(where you want notifications)
- Key:
- Click Save
Adjust Timeout (Optional): ¶
- Still in Configuration tab
- Click General configuration
- Click Edit
- Set Timeout to 30 seconds
- Click Save
Via AWS CLI: ¶
# Package the Lambda function
zip lambda_function.zip lambda_function.py
# Create Lambda function (replace ACCOUNT_ID and REGION)
aws lambda create-function \
--function-name SESNotificationHandler \
--runtime python3.12 \
--role arn:aws:iam::ACCOUNT_ID:role/SESNotificationLambdaRole \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda_function.zip \
--timeout 30 \
--environment Variables="{
SENDER_EMAIL=sender@yourdomain.com,
RECIPIENT_EMAIL=recipient@yourdomain.com
}"
Step 6: Subscribe Lambda to SNS Topic ¶
Via AWS Console: ¶
- Navigate to Amazon SNS console
- Click Topics
- Click on your topic:
ses-bounce-complaint-notifications - Click Create subscription
- Configure:
- Protocol: AWS Lambda
- Endpoint: Select
SESNotificationHandlerfunction
- Click Create subscription
- The subscription should automatically be confirmed (Status: Confirmed)
Via AWS CLI: ¶
# Get Lambda function ARN
LAMBDA_ARN=$(aws lambda get-function \
--function-name SESNotificationHandler \
--query 'Configuration.FunctionArn' \
--output text)
# Subscribe Lambda to SNS topic (replace TOPIC_ARN)
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:ACCOUNT_ID:ses-bounce-complaint-notifications \
--protocol lambda \
--notification-endpoint $LAMBDA_ARN
# Grant SNS permission to invoke Lambda
aws lambda add-permission \
--function-name SESNotificationHandler \
--statement-id AllowSNSInvoke \
--action lambda:InvokeFunction \
--principal sns.amazonaws.com \
--source-arn arn:aws:sns:us-east-1:ACCOUNT_ID:ses-bounce-complaint-notifications
Step 7: Test the Implementation ¶
Test with AWS CLI: ¶
# Send a test email using the configuration set
aws sesv2 send-email \
--from-email-address sender@yourdomain.com \
--destination "ToAddresses=recipient@example.com" \
--content '{
"Simple": {
"Subject": {"Data": "Test Email"},
"Body": {"Text": {"Data": "This is a test email"}}
}
}' \
--configuration-set-name email-tracking-config-set
Test with Python (boto3): ¶
import boto3
ses_client = boto3.client('ses', region_name='us-east-1')
response = ses_client.send_email(
Source='sender@yourdomain.com',
Destination={
'ToAddresses': ['recipient@example.com']
},
Message={
'Subject': {'Data': 'Test Email'},
'Body': {'Text': {'Data': 'This is a test email'}}
},
ConfigurationSetName='email-tracking-config-set'
)
print(f"Email sent! Message ID: {response['MessageId']}")
Simulate a Bounce (for testing): ¶
Use the SES mailbox simulator to test bounce notifications:
aws sesv2 send-email \
--from-email-address sender@yourdomain.com \
--destination "ToAddresses=bounce@simulator.amazonses.com" \
--content '{
"Simple": {
"Subject": {"Data": "Bounce Test"},
"Body": {"Text": {"Data": "Testing bounce notification"}}
}
}' \
--configuration-set-name email-tracking-config-set
You should receive an email notification about the bounce within a few minutes.
Simulate a Complaint (for testing): ¶
aws sesv2 send-email \
--from-email-address sender@yourdomain.com \
--destination "ToAddresses=complaint@simulator.amazonses.com" \
--content '{
"Simple": {
"Subject": {"Data": "Complaint Test"},
"Body": {"Text": {"Data": "Testing complaint notification"}}
}
}' \
--configuration-set-name email-tracking-config-set
Verify the Flow: ¶
- Check Lambda CloudWatch Logs for execution logs
- Check your recipient email inbox for notification emails
- Check SNS Topic metrics to verify messages were published
- Check SES sending statistics in the SES console
Step 8: Apply Configuration Set to Your Emails ¶
Option 1: Using boto3 (Python): ¶
import boto3
ses_client = boto3.client('ses')
response = ses_client.send_email(
Source='sender@yourdomain.com',
Destination={
'ToAddresses': ['customer@example.com']
},
Message={
'Subject': {'Data': 'Your Subject'},
'Body': {
'Text': {'Data': 'Your email content'},
'Html': {'Data': '<h1>Your HTML content</h1>'}
}
},
ConfigurationSetName='email-tracking-config-set' # Add this line
)
Option 2: Using AWS CLI: ¶
aws sesv2 send-email \
--from-email-address sender@yourdomain.com \
--destination "ToAddresses=customer@example.com" \
--content '{
"Simple": {
"Subject": {"Data": "Your Subject"},
"Body": {"Text": {"Data": "Your email content"}}
}
}' \
--configuration-set-name email-tracking-config-set
Option 3: Using SMTP Headers: ¶
If you're using SMTP to send emails, add this header:
X-SES-CONFIGURATION-SET: email-tracking-config-set
Option 4: Set Default Configuration Set (Advanced): ¶
You can set a default configuration set for all emails sent from a verified identity:
aws sesv2 put-email-identity-configuration-set-attributes \
--email-identity sender@yourdomain.com \
--configuration-set-name email-tracking-config-set
Troubleshooting ¶
Issue: Not receiving notification emails ¶
Check:
- Verify both sender and recipient emails are verified in SES
- Check Lambda CloudWatch Logs for errors
- Verify environment variables are set correctly
- Check SES sending limits (sandbox mode restrictions)
- Check spam/junk folder for notification emails
Solution:
# Check Lambda logs
aws logs tail /aws/lambda/SESNotificationHandler --follow
# Test Lambda function directly
aws lambda invoke \
--function-name SESNotificationHandler \
--payload file://test-event.json \
response.json
Issue: Lambda not being triggered ¶
Check:
- Verify SNS subscription is confirmed
- Check Lambda permissions for SNS invocation
- Verify SES configuration set event destinations are enabled
Solution:
# Check SNS subscriptions
aws sns list-subscriptions-by-topic \
--topic-arn arn:aws:sns:REGION:ACCOUNT_ID:ses-bounce-complaint-notifications
# Verify Lambda permissions
aws lambda get-policy --function-name SESNotificationHandler
Issue: Permission denied errors ¶
Check:
- Lambda execution role has correct permissions
- SES sender email is verified
- SNS topic policy allows SES to publish
Solution:
# Update SNS topic policy to allow SES
aws sns set-topic-attributes \
--topic-arn arn:aws:sns:REGION:ACCOUNT_ID:ses-bounce-complaint-notifications \
--attribute-name Policy \
--attribute-value '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ses.amazonaws.com"},
"Action": "SNS:Publish",
"Resource": "arn:aws:sns:REGION:ACCOUNT_ID:ses-bounce-complaint-notifications"
}]
}'
Issue: Configuration set not found ¶
Check:
- Verify configuration set name matches exactly
- Check you're in the correct AWS region
Solution:
# List all configuration sets
aws sesv2 list-configuration-sets
# Verify configuration set details
aws sesv2 get-configuration-set \
--configuration-set-name email-tracking-config-set
Testing Lambda Function Directly ¶
Create a test event file (test-event.json):
{
"Records": [
{
"EventSource": "aws:sns",
"Sns": {
"Message": "{\"notificationType\":\"Bounce\",\"bounce\":{\"bounceType\":\"Permanent\",\"bounceSubType\":\"General\",\"bouncedRecipients\":[{\"emailAddress\":\"bounce@simulator.amazonses.com\",\"action\":\"failed\",\"status\":\"5.1.1\",\"diagnosticCode\":\"smtp; 550 5.1.1 user unknown\"}],\"timestamp\":\"2025-01-01T12:00:00.000Z\"},\"mail\":{\"timestamp\":\"2025-01-01T12:00:00.000Z\",\"source\":\"sender@example.com\"}}"
}
}
]
}
Test with:
aws lambda invoke \
--function-name SESNotificationHandler \
--payload file://test-event.json \
response.json
cat response.json
Monitoring and Maintenance ¶
CloudWatch Metrics to Monitor: ¶
- Lambda Invocations: Number of times Lambda is triggered
- Lambda Errors: Failed executions
- Lambda Duration: Execution time
- SNS Messages Published: Messages sent to SNS topic
- SES Bounce Rate: Overall bounce rate
- SES Complaint Rate: Overall complaint rate
Set Up CloudWatch Alarms: ¶
# Alarm for Lambda errors
aws cloudwatch put-metric-alarm \
--alarm-name SESNotificationLambdaErrors \
--alarm-description "Alert on Lambda function errors" \
--metric-name Errors \
--namespace AWS/Lambda \
--dimensions Name=FunctionName,Value=SESNotificationHandler \
--statistic Sum \
--period 300 \
--evaluation-periods 1 \
--threshold 1 \
--comparison-operator GreaterThanThreshold
# Alarm for high bounce rate
aws cloudwatch put-metric-alarm \
--alarm-name SESHighBounceRate \
--alarm-description "Alert on high bounce rate" \
--metric-name Reputation.BounceRate \
--namespace AWS/SES \
--statistic Average \
--period 3600 \
--evaluation-periods 1 \
--threshold 0.05 \
--comparison-operator GreaterThanThreshold
Best Practices: ¶
- Monitor bounce and complaint rates regularly
- Remove bounced emails from your mailing list
- Review notification emails to identify patterns
- Keep Lambda function updated with latest runtime
- Test configuration changes in a development environment first
- Set up CloudWatch alarms for critical metrics
- Rotate access keys regularly if using programmatic access
- Document any customizations you make to the Lambda function
Additional Resources ¶
- AWS SES Developer Guide
- AWS Lambda Developer Guide
- AWS SNS Developer Guide
- SES Configuration Sets
- SES Event Publishing
Cost Estimation ¶
Approximate monthly costs for 10,000 emails:
- SES: $1.00 (10,000 emails × $0.10/1,000)
- SNS: $0.50 (assuming ~100 bounce/complaint notifications)
- Lambda: $0.00 (within free tier for most use cases)
- CloudWatch Logs: $0.50 (minimal logging)
Total: ~$2.00/month
Note: Costs scale with volume. Always check current AWS pricing.
Security Considerations ¶
- Use IAM roles instead of access keys where possible
- Apply principle of least privilege to IAM policies
- Encrypt sensitive data in environment variables using KMS
- Enable CloudTrail for audit logging
- Regularly review IAM policies and permissions
- Use VPC endpoints if Lambda needs to be in a VPC
- Monitor suspicious activity in CloudWatch Logs
- Keep Python runtime updated to latest version
Next Steps ¶
-
Production Readiness:
- Request SES production access (move out of sandbox)
- Set up CloudWatch dashboards for monitoring
- Implement automated bounce/complaint list management
- Add HTML email support for richer notifications
-
Enhancements:
- Add support for delivery notifications
- Implement notification aggregation (daily digest)
- Add DynamoDB for storing bounce/complaint history
- Create API for querying notification history
- Add support for multiple recipient emails
- Integrate with ticketing systems (Jira, ServiceNow)
-
Scaling:
- Implement DLQ (Dead Letter Queue) for failed Lambda executions
- Add retry logic with exponential backoff
- Consider SQS between SNS and Lambda for buffering
- Implement concurrent execution limits
Support ¶
If you encounter issues:
- Check CloudWatch Logs for detailed error messages
- Review the Troubleshooting section above
- Verify all prerequisites are met
- Test each component individually
- Consult AWS documentation and support forums
For questions or improvements to this guide, please open an issue in the repository.
Comments
Please login to leave a comment.
No comments yet. Be the first to comment!