How to send emails with SES in AWS CDK


Borislav Hadzhiev

Thu Apr 22 20215 min read


Photo by Esther Tuttle

Updated on Thu Apr 22 2021

Sending Emails with SES in AWS CDK #

In this article 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 email 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

npm install
  1. Create an env.ts file in the root directory, providing the variables listed in the env.example.ts file:
  1. Deploy the cdk stack
npx 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 in 3 parameters: name, email, message.

Testing our SES Integration #

Before me move onto 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 them 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:

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

The response should look like:

{ "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.tsfile are verified with the AWS SES service.

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
// ๐Ÿ‘‡ create the lambda that sends emails const mailerFunction = new NodejsFunction(this, 'mailer-function', { runtime: lambda.Runtime.NODEJS_14_X, memorySize: 1024, timeout: cdk.Duration.seconds(3), handler: 'main', entry: path.join(__dirname, '/../src/mailer/index.ts'), });

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:

// ๐Ÿ‘‡ 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}`, ], }), );

In the code snippet, we've 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's we provided via variables the env.ts file.

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

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

We then add the API route at the path /mailer, passing in our mailerFunction as integration:

httpApi.addRoutes({ methods: [apiGateway.HttpMethod.POST], path: '/mailer', integration: new apiGatewayIntegrations.LambdaProxyIntegration({ handler: mailerFunction, }), });

Implementing the Lambda that Sends Emails #

Let's take a look at the implementation of the mailer function at 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:

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 provided in the env.ts file
  • the sender of the email is the SES_EMAIL_FROM variable
  • the Subject of the email is Email from example ses app..

And 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 client's that can't process HTML. Let's look at the implementation of the functions:

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:

npx 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 #

Add me on LinkedIn

I'm a Web Developer with TypeScript, React.js, Node.js and AWS experience.

Let's connect on LinkedIn

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee