How to send emails with SES in AWS CDK

avatar
Borislav Hadzhiev

Last updated: Jan 27, 2024
5 min

banner

# Sending Emails with SES in AWS CDK

We'll go over how to send emails using SES in an application, where the infrastructure is provisioned using AWS CDK.

If you haven't requested a review for your SES identity by filing an AWS Support ticket, you can only send emails to verified email addresses.

You can verify an email address for SES by opening the AWS Console, clicking on Email Addresses in the side menu and then clicking on Verify a New Email Address.

verify email ses

If your SES account is in the sandbox (test) environment, you can only send emails from and to verified email addresses or domains.

To demonstrate the process, we are going to provision a stack that creates the following resources:

  • An API Gateway with a Lambda integration.
  • A Lambda function that uses AWS SES to send an email.
The code for this article is available on GitHub

# Project setup

  1. Clone the github repository.

  2. Install the dependencies.

shell
npm install
  1. Create an env.ts file in the root directory, providing the variables listed in the env.example.ts file:
env.ts
export const SES_REGION = 'YOUR_SES_REGION'; export const SES_EMAIL_TO = 'YOUR_SES_RECIPIENT_EMAIL'; export const SES_EMAIL_FROM = 'YOUR_SES_SENDER_EMAIL';
  1. Deploy the CDK stack.
shell
npx aws-cdk deploy cdk-stack \ --outputs-file ./cdk-outputs.json

At this point we have created an API with a POST method /mailer endpoint that has Lambda integration. The API endpoint invokes a Lambda which uses AWS SES to send an email.

Before we move on to the code, let's test our implementation. The function takes 3 parameters: name, email, message.

# Testing our SES Integration

Before we move on to explaining the code, let's first test the flow of sending emails.

We are going to make a POST request to our API endpoint with Lambda integration. The Lambda function will then invoke the SES APIs to send an email.

You can find the API URL in the cdk-outputs.json file located in the root directory, or by opening the API gateway service in the AWS Console.

Let's send an email.

shell
curl --location --request POST 'YOUR_API_URL/mailer' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "John Smith", "email": "hello@world.com", "message": "Hello world!" }'

The response should look as follows.

api-response
{ "body": { "message": "Email sent successfully ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰" }, "statusCode": 200 }

You should receive an email to the email you specified in the SES_EMAIL_TO variable in the env.ts file.

received email

If you got an error, make sure the email and region you provided in the env.ts file are verified with the AWS SES service.

Also, make sure to check your spam folder if you don't see the email.

# Provisioning the Infrastructure with CDK

The first resource we define is the Lambda function, which is responsible for sending the emails:

The code for this article is available on GitHub
lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ create the lambda that sends emails const mailerFunction = new NodejsFunction(this, 'mailer-function', { runtime: lambda.Runtime.NODEJS_18_X, memorySize: 1024, timeout: cdk.Duration.seconds(3), handler: 'main', entry: path.join(__dirname, '/../src/mailer/index.ts'), });
If you still use CDK version 1, switch to the cdk-v1 branch in the GitHub repository.

It's just a plain function that uses the NodejsFunction construct.

However, since the function is going to talk to SES APIs, we have to add some permissions to it.

lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ Add permissions to the Lambda function to send Emails mailerFunction.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 'ses:SendEmail', 'ses:SendRawEmail', 'ses:SendTemplatedEmail', ], resources: [ `arn:aws:ses:${SES_REGION}:${ cdk.Stack.of(this).account }:identity/${SES_EMAIL_FROM}`, ], }), );

We granted the Lambda permissions to call the ses:SendEmail, ses:SendRawEmail and ses:SendTemplatedEmail actions, because it will be interacting with the SES service.

We have allowed the function to execute the actions on the SES resource we specified in the env.ts file.

The next resource we have defined is the API, which will proxy the request to the Lambda function.

lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ create the API that uses Lambda integration const httpApi = new apiGateway.HttpApi(this, 'api', { apiName: `my-api`, corsPreflight: { // ... cors settings }, });

Then we need to add the API route at the path /mailer and pass it our mailerFunction as integration:

lib/cdk-starter-stack.ts
// ๐Ÿ‘‡ add the /mailer route httpApi.addRoutes({ methods: [apiGateway.HttpMethod.POST], path: '/mailer', integration: new apiGatewayIntegrations.HttpLambdaIntegration( 'mailer-integration', mailerFunction, ), });

# Implementing the Lambda that Sends Emails

Let's take a look at the implementation of the mailer function at src/mailer/index.ts.

src/mailer/index.ts
import {APIGatewayProxyEventV2, APIGatewayProxyResultV2} from 'aws-lambda'; import AWS from 'aws-sdk'; import {SES_EMAIL_FROM, SES_EMAIL_TO, SES_REGION} from '../../env'; export async function main( event: APIGatewayProxyEventV2, ): Promise<APIGatewayProxyResultV2> { try { if (!event.body) throw new Error('Properties name, email and message are required.'); const {name, email, message} = JSON.parse(event.body) as ContactDetails; if (!name || !email || !message) throw new Error('Properties name, email and message are required'); return await sendEmail({name, email, message}); } catch (error: unknown) { console.log('ERROR is: ', error); if (error instanceof Error) { return JSON.stringify({body: {error: error.message}, statusCode: 400}); } return JSON.stringify({ body: {error: JSON.stringify(error)}, statusCode: 400, }); } }

The main function is the entry point of the lambda and it is responsible for validating the user input before it calls the sendEmail function, which calls the SES Apis.

The sendEmail function is, where we invoke the SES Apis and send the emails.

src/mailer/index.ts
async function sendEmail({ name, email, message, }: ContactDetails): Promise<APIGatewayProxyResultV2> { const ses = new AWS.SES({region: SES_REGION}); await ses.sendEmail(sendEmailParams({name, email, message})).promise(); return JSON.stringify({ body: {message: 'Email sent successfully ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰'}, statusCode: 200, }); } function sendEmailParams({name, email, message}: ContactDetails) { return { Destination: { ToAddresses: [SES_EMAIL_TO], }, Message: { Body: { Html: { Charset: 'UTF-8', Data: getHtmlContent({name, email, message}), }, Text: { Charset: 'UTF-8', Data: getTextContent({name, email, message}), }, }, Subject: { Charset: 'UTF-8', Data: `Email from example ses app.`, }, }, Source: SES_EMAIL_FROM, }; }
  • the destination of the email is going to be the SES_EMAIL_TO value we set in the env.ts file
  • the sender of the email is the SES_EMAIL_FROM value
  • the subject of the email is Email from example SES app.

For the body of the email we have provided an HTML version for email clients that can process HTML and a text version for email clients that can't process HTML. Let's look at the implementation of the functions:

src/mailer/index.ts
function getHtmlContent({name, email, message}: ContactDetails) { return ` <html> <body> <h1>Received an Email. ๐Ÿ“ฌ</h1> <h2>Sent from: </h2> <ul> <li style="font-size:18px">๐Ÿ‘ค <b>${name}</b></li> <li style="font-size:18px">โœ‰๏ธ <b>${email}</b></li> </ul> <p style="font-size:18px">${message}</p> </body> </html> `; } function getTextContent({name, email, message}: ContactDetails) { return ` Received an Email. ๐Ÿ“ฌ Sent from: ๐Ÿ‘ค ${name} โœ‰๏ธ ${email} ${message} `; }

# Cleanup

To delete the provisioned resources, execute the destroy command.

shell
npx aws-cdk destroy

# Caveats when sending Emails via SES

In order to send an email using SES we need to do a couple of things:

  • The Email must be sent from a verified SES email address or domain
  • If your account is in the SES sandbox environment, you can only send emails to verified email addresses. To get your account out of the test environment, open a ticket with AWS support on the SES page.
  • The Lambda function calling the SES API has to have the necessary permissions, i.e. ses:SendEmail, ses:SendRawEmail, ses:SendTemplatedEmail.

# Further Reading

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev