Send e-mails through AWS SES and Lambda
Tags: javascript, aws, serverless, lambda
Published: 2019-01-14 18:27:25

---
title: Send e-mails through AWS SES and Lambda
published: true
description: Using Lambda to trigger your e-mails in a simple way using AWS SES.
tags: javascript, aws, serverless, lambda
---

There are several different ways of sending e-mail through your code. There are several platforms and services that might help you achieve it too. In this case I decided to use Lambda and SES (Simple Email Service) from AWS to achieve it and I will explain why:
- All our stack is on AWS which makes it easier to track and monitor everything (bills, metrics and so on...)
- It had to be decoupled from the backend which is written in PHP and is becoming a monolith
- It will run once in a while so it would be nice to not pay while the service is not being used
- I would like to try SES and I'm a big fan of Lambda :p

Let me go through our use case and then share some code and how I did implement it! I assume you are familiar with Node, Lambda and Serverless Framework while showing you the code.

<hr>

# Use case

We are a customer success platform which relies on several metrics to help our client define their customer strategy. One of the metrics we rely on is NPS (Net Promoter Score) which is basically a score that measures your customer satisfaction.

What we had to develop was a way of sending e-mails where the customer would choose a score from 0-10. The e-mail was triggered by the backend after an action from our client. The customer selects a score and it is saved for future analysis.

# Creating our Lambda

First step is creating our Lambda function which would be triggered by the backend. I've used [Serverless Framwork](https://serverless.com/framework/) to do so because it is simple and I already have some experience with it (I actually would like to try [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) next time).

Once we create our project (`npm init`) we've to define the `serverless.yml` file within our configurations. The following is our starting configuration (note I'm using `sa-east-1` as region):

```yml
service:
  name: my-nps-email

provider:
  name: aws
  runtime: nodejs8.10
  region: sa-east-1
  stage: ${opt:stage, "dev"}
  deploymentBucket: my-nps-email-deployment-bucket
  memorySize: 128
  timeout: 5

functions:
  send-email:
    handler: index.handler
```

First remember you've to create the deployment bucket by your own, you can do it via CLI or AWS Console.

Now we've to just create a file called `index.js` and export the `handler` function:

```javascript
exports.handler = (params) => {
  console.log(params);
}
```

Before we deploy make sure you have the Serverless Framework installed (`npm i serverless`). Then you just `npx serverless deploy` and it will be deployed.

Some notes here:
- I like to install it as a development dependency and with exact version (I do update it manually when needed), so I do `npm i --save-dev --save-exact serverless`
- When using `serverless` I always use the flag `-v` which means verbose and shows all stack events during deployment
- When using `serverless` I always set the following environment variable `SLS_DEBUG=*` to enable debugging logs

I do also add some NPM scripts to my `package.json` to make it easier to use:

```json
{
  "name": "my-nps-email",
  "version": "1.0.0",
  "scripts": {
    "deploy": "SLS_DEBUG=* serverless deploy -v"
    "test:valid": "SLS_DEBUG=* serverless invoke --function send-email --path data/valid.json"
  },
  "devDependencies": {
    "serverless": "1.34.1",
  }
}
```

`valid.json` is a valid body that will be called within the Lambda function.

Now we are good and have our function deployed. After that we have to make sure our backend can invoke this function, to do so we have to manage IAM permissions. By default Serverless created an IAM role for you within the following format: `arn:aws:iam::YOUR_ACCOUNT_ID:role/my-nps-email-dev-sa-east-1-lambdaRole`, you can customize it if you want but I like to as it uses the function's name, the stage and the region to create the role name. What we have to do now is to add this role to our backend permissions (Invoke permission in this case):

```json
{
  "Effect": "Allow",
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/my-nps-email-dev-sa-east-1-lambdaRole"
}
```

Now our backend is good to invoke our function. I'm not going into details from how our backend invokes the Lambda function as it is basically copying code from AWS Docs.

Next step is to make our Lambda function send an e-mail from SES.

# Sending e-mail with SES

There is just one setup to use SES: Allows your e-mail to receive e-mails from SES (for testing purposes). When you're ready to go you've to ask AWS to leave what they call Sandbox. After that you can send e-mails "the way you want" (respecting their policies of course).

Go to your Console > SES > Email Addresses > Verify a New Email Address. Follow the steps and you'll be ready to receive e-mails from SES.

As any AWS service you need permission to use it from your service/function/user... So our first step is to allow our Lambda function to call SES. To do so we add an IAM Role Statement to our `serverless.yml`:

```yml
...

provider:
...
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ses:SendEmail"
      Resource:
        - "*"
      Condition:
        StringEquals:
          ses:FromAddress:
            - "fromemail@someemail.com"
...
```

I'm saying that my Lambda is allowed to send e-mails from SES using the From Address *fromemail@someemail.com*. It is just a security check to avoid any console override mistakes.

Now we're going to use the [AWS SDK for Javascript](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html#sendEmail-property) to send e-mails from SES. Our function should receive all the desired parameters to be able to send the e-mail. Below is my current configuration:

```javascript
const AWS = require('aws-sdk');
const SES = new AWS.SES({ region: 'us-east-1' });

exports.handler = async (params)  => {
  console.log(params);

  const {
    to,
    from,
    reply_to: replyTo,
    subject,
  } = params;
  const fromBase64 = Buffer.from(from).toString('base64');

  const htmlBody = `
    <!DOCTYPE html>
    <html>
      <head></head>
      <body><h1>Hello world!</h1></body>
    </html>
  `;

  const sesParams = {
    Destination: {
      ToAddresses: [to],
    },
    Message: {
      Body: {
        Html: {
          Charset: 'UTF-8',
          Data: htmlBody,
        },
      },
      Subject: {
        Charset: 'UTF-8',
        Data: subject,
      },
    },
    ReplyToAddresses: [replyTo],
    Source: `=?utf-8?B?${fromBase64}?= <fromemail@someemail.com>`,
  };

  const response = await SES.sendEmail(sesParams).promise();

  console.log(response);
```

So let's go through this code:
- I do use `console.log` in the beginning and in the end for logging purposes
- I'm using `async/await` as it is better to read the code instead of using promises
- I've few parameters coming from our backend which are used to send our e-mail
- You're probably asking what is that `fromBase64` and what is does. First: you don't need it, you can use the attribute `Source` just with an e-mail but in your inbox it won't look nice when someone receives that e-mail because it won't have the name from the "sender" just its e-mail. I'm doing this trick because I need to deal with `UTF-8` characters and [AWS SES doesn't support SMTPUTF8 extension](https://docs.aws.amazon.com/pt_br/ses/latest/APIReference/API_SendEmail.html) which should be implemented according to [this spec](https://tools.ietf.org/html/rfc2047#section-2). I won't go deeper into it but you can easily follow the section to get to know what each parameter means.

Now our function should be able to send e-mail from SES. Just deploy it again and update your `valid.json` to be used with `npm run test:valid`.

Everything is working now and we can scale if needed (not in this case) and pay for use (It might even be free if you don't go over the free tier).

# Conclusion

As almost everything with serverless we have spent more time with configurations than actually coding but the main difference is that we coded business related things instead of setting up frameworks and libs.

The solution might be "too much" for the problem it addresses but it handles it pretty well with no maintenance and easy to update/deploy if needed.

If you have tips on how to improve this solution and any questions please comment below!