Send transactional emails with AWS SES

Background

I used to recommend Mailgun for a personal project. But as with any commercial products that is generous with free stuff, there's always a catch and it will come back and bite you one way or another, and Mailgun is no exception. Now their free-tier offer is very similar to ALL other email services such as Postmark, Sendgrid and Resend that allows only hundreds of free emails per day and gets really expensive in their PRO plans.

Screw all that.

Enter AWS SES. Now, I really hate working in AWS world. But when it comes to emails, they keep it real. Once you exhausted your free credits, you will be charged at $0.10/1000 emails sent. I know that is not free but $0.10 for sending 1,000 emails can be free if you picked up all the 1 cent coins you ignored on the street. And this path breaks you free from being psychologically manipulated by tricks pulled by all email platforms which led you to constantly hopping and migrating.

Notice there are no screenshots or videos in this guide because they get stale quickly and misleading as AWS changes UI frequently. The goal here is to present all the landmarks you need to navigate the complications of even getting one thing up running in AWS, and start sending transactional emails without bother with other noises from AWS.

Create AWS SES identities

  1. Create AWS account.
  2. Choose your region at top bar.
  3. In left sidebar, ConfigurationIdentities
  4. Click Create identity
  5. Choose Domain → Enter domain(Note: you should already have your website online behind a domain) → Advanced DKIM settingsEasy DKIM → Click Create identity
  6. Create another identiy: Choose Email address → Enter sender email address e.g. human@kudos.wiki → Check Assign a default configuration set → Select my-first-configuration-set from dropdown → Click Create identity
  7. To test sending email to a recipient email address, create an identity for the recipient email address too by repeating Step-6.

Add DKIM and DMARC DNS records in your DNS provider

  1. In left sidebar, Configuration → Click Identities
  2. Click the identity of Domain type you just created.
  3. Under DomainKeys Identified Mail (DKIM) panel → Authentication tab → Click Publish DNS record → Create all three CNAME records in your DNS. Note: Name and Value is Name and Target in Cloudflare DNS respectively.
  4. Under Domain-based Message Authentication, Reporting, and Conformance (DMARC) panel → Click Publish DNS record → Create the TXT record in your DNS.
  5. Note: AWS has already setup the "SPF record" for you. The "MAIL FROM" field of your email will mention "amazonses.com", but this field is out of user's sight. If this still bothers you, you can go down this path. But we just want to send legit emails at practically no cost here, so let's just move on.

Setup configuration set

  1. In left sidebar, Configuration → Click Configuration sets
  2. Click my-first-configuration-set. This has been created for you.
  3. Click Event destinations tab
  4. Click Add destination button
  5. Check boxes for Hard bounces, Complaints, Deliveries and Delivery delays.
  6. Click Next button.
  7. Under Destination options, choose Amazon Cloudwatch as the destination type. Put Name as tracking_per_domain. Check Enabled box under Event publishing.
  8. Under Amazon CloudWatch dimensions, select Value source as Message tag. Dimension name as ses:from-domain. Default value as kudos.wiki.
  9. Click Next button.
  10. Click Add destination button.

Setup IAM

  1. In left side bar, Access managementUsers → Click Create user

  2. Do not check Provide user access to the AWS Management Console → Click Next

  3. Choose Add user to groupCreate groupCreate policy

  4. Copy paste below JSON to the policy

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": ["ses:SendEmail"],
                "Resource": "*",
                "Condition": {
                    "StringEquals": {
                        "ses:FromAddress": "human@kudos.wiki"
                    }
                }
            }
        ]
    }
  5. Select that policy back in the Create Group step.

  6. Click Create user button.

Request AWS SES production access out of sandbox

If they replied with this message:

Hello,

Thank you for submitting your request to increase your sending limits. We would like to gather more information about your use case.

If you can provide additional information about how you plan to use Amazon SES, we will review the information to understand how you are sending and we can recommend best practices to improve your sending experience. In your response, include as much detail as you can about your email-sending processes and procedures.

For example, tell us how often you send email, how you maintain your recipient lists, and how you manage bounces, complaints, and unsubscribe requests. It is also helpful to provide examples of the email you plan to send so we can ensure that you are sending high-quality content that recipients will want to receive.

...

Make sure you give them exactly all that even if you think they mostly don't make sense for your transactional email or whatever you might think.

Here is what worked for me in my second reply after I reopened my case after they rejected me in my first hasty reply:

I realize I didn't provide all the information expected from your end. My apologies for the hasty reply.

Here are the details of my email-sending processes and procedures:

1. Purpose of Emails:
   - I will be sending transactional emails containing license keys to users who have completed a one-time purchase of our product.
   - These are critical, time-sensitive emails that users expect to receive immediately after their purchase.

2. Frequency of Sending:
   - Emails are sent on-demand, triggered only by completed purchases.
   - The frequency directly correlates with our sales volume(i.e. one sale equal to one email is sent), which we anticipate to be less than 5(if at all) emails per day on average, with potential spikes during promotions or launches.

3. Recipient List Maintenance:
   - Our recipient list is maintained using Cloudflare Workers KV storage.
   - Each email address is collected at the point of purchase through our payment provider, Paddle.
   - We only send emails to addresses provided during the purchase process, ensuring high relevance and low bounce rates.

4. Handling Bounces, Complaints, and Unsubscribes:
   - As these are one-time transactional emails, we don't anticipate a high volume of bounces or complaints, because we only send a transactional email to the specific user who has just made a purchase.
   - However, we plan to implement the following measures:
     a. Bounces: We will monitor bounce notifications from AWS SES and remove invalid email addresses from our KV storage to prevent future sending attempts.
     b. Complaints: Although rare for transactional emails, we will honor any complaints by immediately removing the email address in our Cloudflare Workers KV dashboard.
     c. Unsubscribes: While not typically applicable to one-time transactional emails, as seen in the attachment of our sample email, it's stated that user can reply to the email for help.

5. Compliance and Security:
   - We send emails only to verified purchasers, minimizing the risk of unsolicited emails.

Please find below an attachment of the transactional email we would like to send.

Lastly, I have reviewed the AWS Acceptable Use Policy and I can confirm that kudos.wiki doesn't violate it as it's doesn't facilitate or even exchange any physical or virtual goods as mentioned in the FAQ section of the landing page kudos.wiki. It's also worth mentioning that merchant of record Paddle has approved my application and domain to start selling on their platform. For this, I have attached the evidence as well.

I look forward to your review and feedback.


Regards,
Kheoh

To provide the "examples of the email I plan to send", I used https://maily.to/playground to create the email content. Then I simply take a screenshot of it, save it in .png file and attach it.

Sending email with AWS SES API

Once you setup AWS SES stuff, to send email in Nodejs or Cloudflare Workers, here is all you need:

// https://github.com/mhart/aws4fetch/
import { AwsClient } from 'aws4fetch';

const aws = new AwsClient({
    accessKeyId: /** Enter access key of the IAM user you just created */,
    secretAccessKey: /** Enter secret access key of the IAM user you just created */ ,
    region: /** Follow the AWS region of your AWS SES. It's shown on the top-right of AWS SES dashbaord */
});

// "email.ap-southeast-2.amazonaws.com" reference: https://docs.aws.amazon.com/general/latest/gr/ses.html#ses_region
// "/v2/email/outbound-emails" reference: https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html#API_SendEmail_RequestSyntax
aws.fetch('https://email.ap-southeast-2.amazonaws.com/v2/email/outbound-emails', {
    method: 'POST',
    headers: {
        'content-type': 'application/json',
    },
    body: JSON.stringify({
        Destination: {
            ToAddresses: ['hellomyfriend@gmail.com'],
        },
        FromEmailAddress: 'kudos.wiki <human@kudos.wiki>',
        Content: {
            Simple: {
                Subject: {
                    Data: `Thanks for your purchase`,
                },
                Body: {
                    Html: {
                        Data: /** STRING OF HTML CODE OF YOUR EMAIL */,
                    },
                },
            },
        },
    }),
});