AWS SES

AWS provides easy to setup email platform that enables capability to send emails from application. Application developers can create template that managed outside of application code. DevOps can create configuration set to based on use cases that limits, adds secrity and improve reputation.

Purpose

In this page, we are sharing basics of SES setup and value of properly managing suppression lists and virtual delivery manager.

Identities

Identity is about expressing our ownership towards a domain or individual email addresses that we work with. AWS platform allows us to begin work with sandbox environment to begin the development without waiting. It guides us setup DKIM, SPF, DMARC and BINI to increase reputation and guarentee the deliverability at each identity level.

Creating identity (easy to do in console with workflow - preferred)

$ aws sesv2 create-email-identity --email-identity mm-notes.com
{
    "IdentityType": "DOMAIN",
    "VerifiedForSendingStatus": false,
    "DkimAttributes": {
        "SigningEnabled": true,
        "Status": "NOT_STARTED",
        "Tokens": [
            "token-1",
            "token-2",
            "token-3"
        ],
        "SigningAttributesOrigin": "AWS_SES",
        "NextSigningKeyLength": "RSA_2048_BIT",
        "CurrentSigningKeyLength": "RSA_2048_BIT",
        "LastKeyGenerationTimestamp": 1754653048.28
    }
}
$ aws sesv2 get-email-identity --email-identity mm-notes.com
{
    "IdentityType": "DOMAIN",
    "FeedbackForwardingStatus": true,
    "VerifiedForSendingStatus": false,
    "DkimAttributes": {
        "SigningEnabled": true,
        "Status": "PENDING",
        "Tokens": [
            "token-1",
            "token-2",
            "token-3"
        ],
        "SigningAttributesOrigin": "AWS_SES",
        "NextSigningKeyLength": "RSA_2048_BIT",
        "CurrentSigningKeyLength": "RSA_2048_BIT",
        "LastKeyGenerationTimestamp": 1754653048.28
    },
    "MailFromAttributes": {
        "BehaviorOnMxFailure": "USE_DEFAULT_VALUE"
    },
    "Policies": {},
    "Tags": [],
    "VerificationStatus": "PENDING",
    "VerificationInfo": {}
}

Once SES identity created, it will be in Verification pending status. To validate, we need to create TXT DNS records based on the Tokens under DkimAttributes. These record value ends with dkim.amazonses.com. If you are using AWS Route53 as your DNS, you may push a button Publish DNS records to Route53 on the console to publish. Alternatively you can download the record set and work with your DNS provider.

We have to wait for AWS to validate the DNS entries. Until then if we attempt sending email, we get error message like below.

dig CNAME token._domainkey.mm-notes.com
nslookup -type=CNAME token._domainkey.mm-notes.com
$ aws ses send-email \
     --from "no-reply@mm-notes.com" \
     --destination "ToAddresses=user@yopmail.com" \
     --message "Subject={Data=Test Email},Body={Text={Data=Hello World}}"

An error occurred (MessageRejected) when calling the SendEmail operation: Email address is not verified. The following identities failed the check in region US-EASTT-1: user@yopmail.com, no-reply@mm-notes.com

Once the setup successful,

$ aws ses send-email \
>     --from "no-reply@mm-notes.com" \
>     --destination "ToAddresses=mahendran@yopmail.com" \
>     --message "Subject={Data=Test Email},Body={Text={Data=Hello World}}"
{
    "MessageId": "random-uuid-000000"
}

Suppression list

Suppression list is being built by SES over a period of time based on its attempt to deliver mails to target list. We can also import suppression list which is helpful if we have multiple environment and when needed to follow the “Unsubscribe / Do Not Distrub” guidelines.

  • Review periodically
  • Share data with application/business team which sends email

While AWS console is not allowing to export all suppression list, we can export it with below python script. This is useful to share with business team to aware and act.

import boto3
import csv
from datetime import datetime

def get_ses_suppressions():
    # Initialize SESv2 client for SES service region
    ses_client = boto3.client('sesv2', region_name='us-east-1')
    
    # Lists to store different types of suppressions
    suppressions = []
    
    # Get current timestamp for filename
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    
    try:
        # Get all suppressed destinations with manual pagination using NextToken
        next_token = None
        
        while True:
            # Prepare parameters for API call
            params = {'Reasons': ['BOUNCE', 'COMPLAINT']}
            if next_token:
                params['NextToken'] = next_token
                
            # Make API call
            response = ses_client.list_suppressed_destinations(**params)
            
            # Process results
            for item in response.get('SuppressedDestinationSummaries', []):
                suppressions.append({
                    'email': item['EmailAddress'],
                    'reason': item['Reason'],
                    'last_update_time': item['LastUpdateTime'].strftime('%Y-%m-%d %H:%M:%S'),
                    'attributes': str(item.get('Attributes', {}))
                })
            
            # Check if there are more results
            next_token = response.get('NextToken')
            if not next_token:
                break
                
        # Write to CSV file
        output_file = f'ses_suppressions_{timestamp}.csv'
        
        with open(output_file, 'w', newline='') as csvfile:
            fieldnames = ['email', 'reason', 'last_update_time', 'attributes']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            
            writer.writeheader()
            for suppression in suppressions:
                writer.writerow(suppression)
                
        print(f"Successfully exported {len(suppressions)} suppressions to {output_file}")
        
    except Exception as e:
        print(f"Error occurred: {str(e)}")

if __name__ == "__main__":
    get_ses_suppressions()

We can also use AWS cli to periodically check new supressions

$ aws sesv2 list-suppressed-destinations --start-date $(date -d "14 days ago" +%s)

Virtual Delivery Manager

  • Additional cost but valuable service.
  • Help to find reasons for mail delivery failures
  • provides dashboard with metrics about send, open and click rates.
$ aws sesv2 create-export-job \
    --export-data-source '{
        "MessageInsightsDataSource": {
            "StartDate": '$(date -d "1 days ago" +%s)',
            "EndDate": '$(date +%s)',
            "Include": {
                "LastDeliveryEvent": ["PERMANENT_BOUNCE","TRANSIENT_BOUNCE", "COMPLAINT", "UNDETERMINED_BOUNCE"]
            }
        }
    }' \
    --export-destination '{
        "DataFormat": "CSV"
    }'

The above command will return an job-id. We can check the job detais with following command. When it completes, it will have a pre-signed url for the export. (Not in our AWS account - Security and Privacy?)

$ aws sesv2 get-export-job --job-id <your-job-id>

Configuration Sets

  • By using configuration sets, we can address individual application team needs in logging, suppression, archiving by overriding account level settings.
  • Having separate configuration sets for application teams, environments helps to manage (pause/monitor) smaller subset.
  • If you have a need of keeping mail audit logs (not the body of email) for long time, you can setup SNS event destinations.