Last updated: Jan 27, 2024
Reading timeยท5 min
We'll go over how to send emails using SES in an application, where the infrastructure is provisioned using AWS CDK.
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.
To demonstrate the process, we are going to provision a stack that creates the following resources:
Clone the github repository.
Install the dependencies.
npm install
env.ts
file in the root directory, providing the variables listed
in the env.example.ts
file: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';
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
.
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.
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": "hello@world.com", "message": "Hello world!" }'
The response should look as follows.
{ "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.
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.
The first resource we define is the Lambda function, which is responsible for sending the emails:
// ๐ 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'), });
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}`, ], }), );
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.
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 }, });
Then we need to add the API route at the path /mailer
and pass it our
mailerFunction
as integration:
// ๐ add the /mailer route httpApi.addRoutes({ methods: [apiGateway.HttpMethod.POST], path: '/mailer', integration: new apiGatewayIntegrations.HttpLambdaIntegration( 'mailer-integration', mailerFunction, ), });
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, }; }
SES_EMAIL_TO
value we set in
the env.ts
fileSES_EMAIL_FROM
valueEmail 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:
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} `; }
To delete the provisioned resources, execute the destroy
command.
npx aws-cdk destroy
In order to send an email using SES we need to do a couple of things:
SES
API has to have the necessary
permissions, i.e. ses:SendEmail
, ses:SendRawEmail
,
ses:SendTemplatedEmail
.