Table of Contents
Receiving email via AWS SES
Posted on
Problem
I want to be able to receive emails for my blog-hosting domain. However, I do not want to take care of running my own email server. Everything should be configured fairly simple and costs should only occur if I really make use of the email service.
Idea
AWS offers its Simple Emails Service (SES), a solution for sending and receiving emails1. Costs only occur if emails are sent or received. Furthermore, it is possible to send a notification via AWS’s Simple Notification Service (SNS) when emails are received. Incoming emails can be stored in s3.
Solution
AWS SES offers everything I need for receiving emails. The setup consists of only a few configuration steps and therefore I will give it a try. I will mainly follow the instructions from the AWS developer guide for SES2. On a high level setting up email reception consists of the following steps:
- Domain verification for SES.
- Publishing an MX record.
- Give SES access to other AWS resources e.g., for storing emails to s3 or sending notifications via SES.
Setting up domain verification
The first step of configuring email reception via SES is verifying that I own and control the recipient domain. Domain verification is done via DKIM and can be configured in two ways. Either via Easy DKIM3 or via Bring You Own DKIM4. I chose the use Easy DKIM because for the moment I don’t want to take care of handling any encryption keys. To configure Easy DKIM it is necessary to first create a domain identity.
This domain identity can be created via the CLI with the following command.
$ aws sesv2 create-email-identity --email-identity allcomputersarebroken.org --tags "Key=application,Value=blog" { "IdentityType": "DOMAIN", "FeedbackForwardingStatus": true, "VerifiedForSendingStatus": false, "DkimAttributes": { "SigningEnabled": true, "Status": "PENDING", "Tokens": [ "b7bgv5gsp3ynze4yip6hak6w4jllmzql", "sdkykziocayal3t35qmgieniyepadlpa", "3ri6g5tbmmzymy7wcco32mecsp4swz7q" ], "SigningAttributesOrigin": "AWS_SES", "NextSigningKeyLength": "RSA_2048_BIT", "CurrentSigningKeyLength": "RSA_2048_BIT", "LastKeyGenerationTimestamp": "2024-01-12T12:15:12.201000+01:00" }, "MailFromAttributes": { "BehaviorOnMxFailure": "USE_DEFAULT_VALUE" }, "Policies": {}, "Tags": [ { "Key": "application", "Value": "blog" } ], "VerificationStatus": "PENDING", "VerificationInfo": {} }
This creates the domain identity. The object DkimAttributes
is of interest
when it comes to validating the domain identity.
To check whether domain is already verified the below command can be used.
$ aws sesv2 list-email-identities { "EmailIdentities": [ { "IdentityType": "DOMAIN", "IdentityName": "allcomputersarebroken.org", "SendingEnabled": false, "VerificationStatus": "PENDING" } ] }
The output shows that the domain identity was not yet verified. Therefore, I now have to create the appropriate verification. Since I chose to use Easy DKIM, the verification will consist of publishing three CNAME records in the DNS zone of my domain.
These CNAME records have the following structure which I found in the AWS web
console for SES under AWS SES > Verified identities >
allcomputersarebroken.org > Authentication
Record name | Record type | value |
---|---|---|
<TOKEN>._domainkey.allcomputersarebroken.org | CNAME | <TOKEN>.dkim.amazonses.com |
For <TOKEN>
the values of the Tokens
as returned by the command that
created the domain identity must be used.
[...] "DkimAttributes": { "SigningEnabled": true, "Status": "PENDING", "Tokens": [ "b7bgv5gsp3ynze4yip6hak6w4jllmzql", "sdkykziocayal3t35qmgieniyepadlpa", "3ri6g5tbmmzymy7wcco32mecsp4swz7q" ], [...]
To set the DNS records in Route53, I have to use the
change-resource-record-sets
sub-command of the route53
command5.
This sub-command expects a JSON object with all the changes as input. As I do
not know the required JSON structure by heart, I can use the cli to create a
skeleton of it.
aws route53 change-resource-record-sets --generate-cli-skeleton > resource_record_for_dkim.json
After editing the skeleton, the JSON looks as follows.
{ "HostedZoneId": "/hostedzone/Z06540432HSWGHY1RCPI1", "ChangeBatch": { "Comment": "set CNAME records required for Easy DKIM domain verification", "Changes": [ { "Action": "CREATE", "ResourceRecordSet": { "Name": "b7bgv5gsp3ynze4yip6hak6w4jllmzql._domainkey.allcomputersarebroken.org", "Type": "CNAME", "TTL": 60, "ResourceRecords": [ { "Value": "b7bgv5gsp3ynze4yip6hak6w4jllmzql.dkim.amazonses.com" } ] } }, { "Action": "CREATE", "ResourceRecordSet": { "Name": "sdkykziocayal3t35qmgieniyepadlpa._domainkey.allcomputersarebroken.org", "Type": "CNAME", "TTL": 60, "ResourceRecords": [ { "Value": "sdkykziocayal3t35qmgieniyepadlpa.dkim.amazonses.com" } ] } }, { "Action": "CREATE", "ResourceRecordSet": { "Name": "3ri6g5tbmmzymy7wcco32mecsp4swz7q._domainkey.allcomputersarebroken.org", "Type": "CNAME", "TTL": 60, "ResourceRecords": [ { "Value": "3ri6g5tbmmzymy7wcco32mecsp4swz7q.dkim.amazonses.com" } ] } } ] } }
The DNS records can be created via
$ aws route53 change-resource-record-sets --cli-input-json file://resource_record_for_dkim.json { "ChangeInfo": { "Id": "/change/C01339613REKF7CHKWE12", "Status": "PENDING", "SubmittedAt": "2024-01-12T11:59:17.639000+00:00", "Comment": "set CNAME records required for Easy DKIM domain verification" } }
To check the status of the record creation, I can ask route53. After a few
minutes, the status INSYNC
should be returned, indicating that the records
are propagated.
$ aws route53 get-change --id /change/C01339613REKF7CHKWE12 { "ChangeInfo": { "Id": "/change/C01339613REKF7CHKWE12", "Status": "INSYNC", "SubmittedAt": "2024-01-12T11:59:17.639000+00:00", "Comment": "set CNAME records required for Easy DKIM domain verification" } }
It can take a while until SES verifies the DNS entries. To check if the domain identity
is already validated, the command get-email-identity
sub-command can be used.
$ aws sesv2 get-email-identity --email-identity allcomputersarebroken.org { "IdentityType": "DOMAIN", "FeedbackForwardingStatus": true, "VerifiedForSendingStatus": false, "DkimAttributes": { "SigningEnabled": true, "Status": "PENDING", "Tokens": [ "b7bgv5gsp3ynze4yip6hak6w4jllmzql", "sdkykziocayal3t35qmgieniyepadlpa", "3ri6g5tbmmzymy7wcco32mecsp4swz7q" ], "SigningAttributesOrigin": "AWS_SES", "NextSigningKeyLength": "RSA_2048_BIT", "CurrentSigningKeyLength": "RSA_2048_BIT", "LastKeyGenerationTimestamp": "2024-01-12T12:15:12.201000+01:00" }, "MailFromAttributes": { "BehaviorOnMxFailure": "USE_DEFAULT_VALUE" }, "Policies": {}, "Tags": [ { "Key": "application", "Value": "blog" } ], "VerificationStatus": "PENDING", "VerificationInfo": { "LastCheckedTimestamp": "2024-01-12T12:44:14.251000+01:00", "ErrorType": "HOST_NOT_FOUND", "SOARecord": { "PrimaryNameServer": "ns-1146.awsdns-15.org", "AdminEmail": "awsdns-hostmaster.amazon.com", "SerialNumber": 1 } } }
Once the VerificationStatus
is SUCCESS
the domain is verified and ready
to use DKIM.
Creating a proper MX record
The next step is to create a MX record in Route53. To do so, I just copied the previously created JSON file that contains the resource records for the domain verification and adjusted it so that it will create a proper MX record.
cp resource_record_for_dkim.json resource_record_for_mx.json
The modified resource record JSON file looks as follows.
{ "HostedZoneId": "/hostedzone/Z06540432HSWGHY1RCPI1", "ChangeBatch": { "Comment": "set CNAME records required for Easy DKIM domain verification", "Changes": [ { "Action": "CREATE", "ResourceRecordSet": { "Name": "allcomputersarebroken.org", "Type": "MX", "TTL": 60, "ResourceRecords": [ { "Value": "10 inbound-smtp.eu-central-1.amazonaws.com" } ] } } ] } }
To create the resource record, I use the AWS cli like I used it for creating the records for the domain verification.
$ aws route53 change-resource-record-sets --cli-input-json file://resource_record_for_mx.json { "ChangeInfo": { "Id": "/change/C0596587U836SX246A4R", "Status": "PENDING", "SubmittedAt": "2024-01-12T14:40:39.888000+00:00", "Comment": "set CNAME records required for Easy DKIM domain verification" } }
Configuring what happens when an email is received
The last step to configure email reception with AWS is to define what happens with the email once it is received by SES. In my case I want to be notified once an email is received and I want to store the emails in a dedicated s3 bucket.
For SES to be able to send notifications and to store emails to the s3 bucket, it is mandatory to assign proper permissions.
Since I want a dedicated s3 bucket for all the emails, I have to create it first.
$ aws s3api create-bucket --bucket mails.allcomputersarebroken.org --region eu-central-1 --create-bucket-configuration LocationConstraint=eu-central-1 { "Location": "http://mails.allcomputersarebroken.org.s3.amazonaws.com/" }
The next step is to create the SNS topic that will be used for notification.
$ aws sns create-topic --name email-received-to-blog --tags 'Key=application,Value=blog' { "TopicArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog" }
Next I need to subscribe to the topic via email.
$ aws sns subscribe --topic-arn arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog \ --protocol email --notification-endpoint <EMAIL_ADDRESS> { "SubscriptionArn": "pending confirmation" }
I can check all current subscriptions to any topic too.
$ aws sns list-subscriptions { "Subscriptions": [ { "SubscriptionArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog:e9656218-02ed-4e38-96fd-d9e4d1f5b753", "Owner": "<ACCOUNT_ID>", "Protocol": "email", "Endpoint": "<EMAIL_ADDRESS>", "TopicArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog" } ] }
Now that the bucket and the SNS topic are created, I have to configure a bucket policy to grant SES access to the bucket. AWS offers an example bucket policy that I adjusted to my specific needs.
{ "Version":"2012-10-17", "Statement":[ { "Sid":"AllowSESPuts", "Effect":"Allow", "Principal":{ "Service":"ses.amazonaws.com" }, "Action":"s3:PutObject", "Resource":"arn:aws:s3:::mails.allcomputersarebroken.org/*", "Condition":{ "StringEquals":{ "AWS:SourceAccount":"<ACCOUNT_ID>", "AWS:SourceArn": "arn:aws:ses:eu-central-1:<ACCOUNT_ID>:receipt-rule-set/rule_set_name:receipt-rule/receipt_rule_name" } } } ] }
The bucket policy above already contains the s3 bucket ARN and the AWS
account ID. To complete the policy, I also need the name of the receipt rule
set
that contains the receipt rule
which contains the deliver to s3
bucket
action.
This means, I now have to create a receipt rule set and a receipt rule. To
create a receipt rule set, I use the create-receipt-rule-set
sub-command of
the ses
command.
$ aws ses create-receipt-rule-set --rule-set-name mails.allcomputersarebroken.org
It is also possible to print information about the receipt rule set.
$ aws ses describe-receipt-rule-set --rule-set-name mails.allcomputersarebroken.org { "Metadata": { "Name": "mails.allcomputersarebroken.org", "CreatedTimestamp": "2024-01-12T18:17:49.749000+00:00" }, "Rules": [] }
Next I have to create a receipt rule. To ease the configuration, I create a CLI-skeleton first.
$ aws ses create-receipt-rule --generate-cli-skeleton > receipt_rule.json
The final receipt rule looks as follows.
{ "RuleSetName": "mails.allcomputersarebroken.org", "After": "", "Rule": { "Name": "deliver2s3", "Enabled": true, "TlsPolicy": "Require", "Actions": [ { "S3Action": { "TopicArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog", "BucketName": "mails.allcomputersarebroken.org", "ObjectKeyPrefix": "" } } ], "ScanEnabled": true } }
Before creating the receipt rule, I have to complete the configuration of the bucket policy.
After updating the bucket policy with the receipt rule set and the receipt rule, it looks as follows.
{ "Version":"2012-10-17", "Statement":[ { "Sid":"AllowSESPuts", "Effect":"Allow", "Principal":{ "Service":"ses.amazonaws.com" }, "Action":"s3:PutObject", "Resource":"arn:aws:s3:::mails.allcomputersarebroken.org/*", "Condition":{ "StringEquals":{ "AWS:SourceAccount":"<ACCOUNT_ID>", "AWS:SourceArn": "arn:aws:ses:eu-central-1:<ACCOUNT_ID>:receipt-rule-set/mails.allcomputersarebroken.org:receipt-rule/deliver2s3" } } } ] }
To apply the policy to the bucket, I have to run the following command.
aws s3api put-bucket-policy --bucket mails.allcomputersarebroken.org \
--policy file://ses_bucket_policy.json
Once the policy is applied to the bucket, I should be able to create the receipt rule.
$ aws ses create-receipt-rule --cli-input-json file://receipt_rule.json An error occurred (RuleDoesNotExist) when calling the CreateReceiptRule operation: Rule does not exist:
This gives me an error that the rule does not exist. However, I can try a different approach. First, I have to modify the receipt rule.
{ "Name": "deliver2s3", "Enabled": true, "TlsPolicy": "Require", "Actions": [ { "S3Action": { "TopicArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog", "BucketName": "mails.allcomputersarebroken.org", "ObjectKeyPrefix": "" } } ], "ScanEnabled": true }
The file now only contains the Rule
part of the previous receipt rule. I
save the content to the file receipt_rule.json
so that I can reference it
in am moment. With this new file, I can try to configure the receipt rule
again.
$ aws ses create-receipt-rule --cli-input-json file://receipt_rule.json
The command returns no error and I assume it worked correctly. To verify this, I can request the description of the receipt rule set to which the receipt rule belongs.
$ aws ses describe-receipt-rule-set --rule-set-name mails.allcomputersarebroken.org { "Metadata": { "Name": "mails.allcomputersarebroken.org", "CreatedTimestamp": "2024-01-12T18:17:49.749000+00:00" }, "Rules": [ { "Name": "deliver2s3", "Enabled": true, "TlsPolicy": "Require", "Actions": [ { "S3Action": { "TopicArn": "arn:aws:sns:eu-central-1:<ACCOUNT_ID>:email-received-to-blog", "BucketName": "mails.allcomputersarebroken.org", "ObjectKeyPrefix": "" } } ], "ScanEnabled": true } ] }
Lastly, I have to set the active receipt rule set to the previously created rule set.
$ aws ses set-active-receipt-rule-set --rule-set-name mails.allcomputersarebroken.org
After this, I can send a test mail to my blog email address to see if it works. Fortunately, I see the email delivered to s3 and I also receive an email notifying me about the fact that an item was added to the s3 bucket.