Skip to content

Join the Seedly owners community →

Email

Setting Up Amazon SES

Verify your domain, set up DKIM and SPF and DMARC, get out of the sandbox, and send your first email from Node

Written by 14 min read2 activities
Pixl, your presenter

Pixl presents

AWS setup is a maze of acronyms. DKIM, SPF, DMARC, IAM. We'll walk it anyway and send a real email.AWS setup is a maze of acronyms. DKIM, SPF, DMARC, IAM. We'll walk it anyway and send a real email.

Pixl fits the last wooden puzzle piece into an envelope shaped frame
Many small setup pieces add up to one real email

This lesson takes you from an empty SES account all the way to a real email sent from your own code. Pick one AWS Region before you start, and do EVERY step in that same Region.

Creating Identities#

In SES, an identity is something you're allowed to send from. There are two kinds.

  • Email address identity. One single address, like [email protected]. AWS calls this "the fastest way to get started."
  • Domain identity. A whole domain, like yourapp.com. Once it's verified you can send from any address or subdomain under it without verifying each one.

You can have both. For a real app, the domain identity is the one you want. The email address identity is handy for quick tests, especially while you're stuck in the sandbox.

Step 1. Create a Domain Identity#

Open the SES console. Under Configuration, choose Identities, then Create identity. Select Domain and type in your domain.

If your site lives at www.yourapp.com, enter yourapp.com. AWS warns that including the "www." part makes verification fail.

Leave DKIM signatures enabled and choose Create identity.

Step 2. Add the DKIM Records to Your DNS#

SES now shows you three CNAME records under Publish DNS records. Copy them into your domain's DNS settings, wherever your domain is managed.

Copy the record names EXACTLY as shown. The underscore in each name is required, and AWS specifically warns against adding an extra underscore at the start.

If your domain is managed in Amazon Route 53 on the same AWS account, SES can publish these records for you.

Step 3. Wait for Verification#

AWS says DNS changes "can take up to 72 hours to propagate." As soon as SES finds all three records, the identity status flips to Verified.

Step 4. Create an Email Address Identity for Testing#

Back in Identities, choose Create identity again, select Email address and enter an inbox you can actually open. AWS sends a verification email from [email protected]. Click the link inside.

The link expires after 24 hours. If it runs out, use the Resend option on the identity's page.

While you're in the sandbox, every address you send TO has to be verified too, so this test inbox is where your first emails will land.

DKIM, SPF and DMARC in Plain Words#

You met these three in the last chapter. Here's how they fit together in SES, using the descriptions straight from the SES docs.

DKIM Signs Every Email#

DKIM adds a digital signature to each message. The three CNAME records you just added are what the SES docs call Easy DKIM. With Easy DKIM, AWS says SES "automatically adds a 2048-bit DKIM key to every email that you send from that identity." You don't have to sign anything in your code.

SPF Lists Who May Send#

Every email actually has two senders. The From address is the one people see. The MAIL FROM address is a hidden one used for bounces. By default, SES uses a subdomain of amazonses.com as the MAIL FROM.

To use your own domain there, you set up a custom MAIL FROM domain. Pick a subdomain you don't use for anything else, like bounce.yourapp.com. In the SES console, open your verified domain, find the Custom MAIL FROM domain section, choose Edit and enter that subdomain. SES then shows you two records to add, in these formats.

NameTypeValue
bounce.yourapp.comMX10 feedback-smtp.REGION.amazonses.com
bounce.yourapp.comTXT"v=spf1 include:amazonses.com ~all"

REGION will be the code of the Region you're using, such as us-east-1. Copy the exact values the console shows you instead of typing them out. The MAIL FROM domain has to have exactly one MX record, or the setup fails.

DMARC Ties It Together#

DMARC checks that the From address people see actually matches what SPF or DKIM checked. The SES docs explain that a message passes DMARC if either the SPF check or the DKIM check passes with matching domains.

You set DMARC with one TXT record named _dmarc.yourapp.com. The SES docs give this example value.

"v=DMARC1;p=quarantine;rua=mailto:[email protected]"

The p= part is the policy. AWS recommends rolling it out slowly, starting with p=none to only collect reports, then moving to p=quarantine, and finally p=reject once you know your legit mail passes.

Once all your records are in, it's worth running your domain through a checker before you trust it. I built a free email and DNS deliverability checker for exactly this reason, because squinting at TXT records by hand gets old FAST.

Getting Out of the Sandbox#

Once your domain shows Verified, request production access. In the SES console, open Account dashboard, choose View Get set up page, then Request production access.

Pick Marketing or Transactional, add your website URL and tick the acknowledgement that you only email people who asked for it and that you handle bounces and complaints. AWS gives an initial response within 24 hours.

You can keep building while you wait. Sending to your verified test inbox works the whole time.

Credentials Without the Risk#

Pixl puts away a giant key ring and holds out one small key that fits a mailbox
Give your app only the access it needs to send email

Your code needs credentials to call SES. For the SES API those are AWS access keys, which come in two parts, an access key ID and a secret access key.

Here's AWS's guidance, kept short.

  • Never use your root account keys. The SES docs say to use IAM user credentials instead, because root credentials "grant full access to all your AWS resources."
  • Prefer temporary credentials. AWS recommends that workloads use temporary credentials from IAM roles. If your code runs on an AWS service like Lambda or EC2, it can get role credentials automatically, with no keys to copy around.
  • Use long-term keys only when you have to. If your app runs somewhere that can't use a role, an IAM user with access keys is the fallback. Update those keys when needed.
  • Grant only what's needed. AWS calls this least privilege. The identity your app uses should be allowed to send email, and managing your whole account is way outside its job.

Keep the keys out of your code. The AWS SDK reads these environment variables on its own.

AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_REGION=us-east-1

Set them in your hosting platform's environment settings. On your own computer, put them in a .env file inside your project folder (the ses-test folder you're about to make), and make sure it never, ever gets committed to git. Node.js doesn't read .env on its own, so you'll point it at the file when you run your code in Step 4.

Sending Your First Email in Node#

This example follows the AWS SDK for JavaScript v3 guide, which uses the @aws-sdk/client-ses package and its SendEmailCommand.

Step 1. Set Up the Project#

The AWS code below uses import lines and a top-level await, so Node.js has to treat your project as an ES module. Following the AWS Get started with Node.js guide, make a new folder, create a package.json, mark the project as a module and install the SES client.

mkdir ses-test
cd ses-test
npm init -y
npm pkg set type=module
npm install @aws-sdk/client-ses

npm pkg set type=module adds "type": "module" to your package.json. Skip it and Node.js reads .js files the old CommonJS way and trips over the very first import line.

You also need a recent Node.js. The AWS SDK for JavaScript v3 README says current versions of the SDK require Node.js 20 or higher. Run node --version to check yours.

Step 2. Create the Client#

Make a libs folder with a file called sesClient.js. This is the AWS example with one change. It reads AWS_REGION from your environment and falls back to us-east-1, where the original hard-codes a Region. Either way, the Region has to be the one where you verified your identities.

import { SESClient } from "@aws-sdk/client-ses";
// Set the AWS Region. Use the Region where your identities are verified.
const REGION = process.env.AWS_REGION ?? "us-east-1";
// Credentials are automatically resolved using the AWS SDK credential provider chain.
// For more information, see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
// Create SES service object.
const sesClient = new SESClient({ region: REGION });
export { sesClient };

Notice there are zero keys in this file. The SDK looks for credentials in a fixed order, and environment variables sit near the front of that line.

Step 3. Write the Send Function#

Create ses_sendemail.js next to the libs folder. This one's also the AWS example as written.

import { SendEmailCommand } from "@aws-sdk/client-ses";
import { sesClient } from "./libs/sesClient.js";
 
const createSendEmailCommand = (toAddress, fromAddress) => {
  return new SendEmailCommand({
    Destination: {
      /* required */
      CcAddresses: [
        /* more items */
      ],
      ToAddresses: [
        toAddress,
        /* more To-email addresses */
      ],
    },
    Message: {
      /* required */
      Body: {
        /* required */
        Html: {
          Charset: "UTF-8",
          Data: "HTML_FORMAT_BODY",
        },
        Text: {
          Charset: "UTF-8",
          Data: "TEXT_FORMAT_BODY",
        },
      },
      Subject: {
        Charset: "UTF-8",
        Data: "EMAIL_SUBJECT",
      },
    },
    Source: fromAddress,
    ReplyToAddresses: [
      /* more items */
    ],
  });
};
 
const run = async () => {
  const sendEmailCommand = createSendEmailCommand(
    "[email protected]",
    "[email protected]",
  );
 
  try {
    return await sesClient.send(sendEmailCommand);
  } catch (caught) {
    if (caught instanceof Error && caught.name === "MessageRejected") {
      /** @type { import('@aws-sdk/client-ses').MessageRejected} */
      const messageRejectedError = caught;
      return messageRejectedError;
    }
    throw caught;
  }
};

Swap "[email protected]" for your verified test inbox and "[email protected]" for an address on your verified domain. Then replace the subject and the two body placeholders with real text.

Source is the From address. Html and Text are two versions of the same message, one for email apps that show HTML and one for the ones that only show plain text.

Step 4. Call It and Run It#

The snippet above defines run but never actually calls it (sneaky, right?). Add this line at the bottom of the file so it runs and prints the result.

console.log(await run());

Then run the file from inside your ses-test folder. The --env-file flag tells Node.js to load your keys from .env first.

node --env-file=.env ses_sendemail.js

This only works because Step 1 turned the project into an ES module. The import lines and the await sitting outside a function both need it. If you'd rather use require, check out the AWS page on ES6 and CommonJS syntax.

TL;DR#

  • Verify a domain identity for real sending and an email address identity for quick tests.
  • Easy DKIM uses three CNAME records, and SES signs every email for you once they verify.
  • A custom MAIL FROM domain adds one MX record and one SPF TXT record on a spare subdomain.
  • DMARC is one TXT record at _dmarc. Start with p=none and tighten it slowly.
  • Request production access once your domain is verified, since the sandbox only sends to verified addresses.
  • Keep access keys in environment variables, never use root keys and prefer IAM roles when your code runs on AWS.
  • @aws-sdk/client-ses with SendEmailCommand sends your first email in a few dozen lines.

What's Next?#

Before you send to real users, set up a way to handle bounces and complaints. The SES docs cover using Amazon SNS notifications for exactly that...

This lesson ends with 2 short activities.