DEV Community

Vinicius Kiatkoski Neves for Centrics

Posted on

Send e-mails through AWS SES and 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.


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 to do so because it is simple and I already have some experience with it (I actually would like to try AWS SAM 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):

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
Enter fullscreen mode Exit fullscreen mode

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:

exports.handler = (params) => {
  console.log(params);
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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",
  }
}
Enter fullscreen mode Exit fullscreen mode

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):

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

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:

...

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

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 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:

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);
Enter fullscreen mode Exit fullscreen mode

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 which should be implemented according to this spec. 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!

Top comments (2)

Collapse
 
studioo profile image
Nick

Thank you for article. It helped me.

Collapse
 
viniciuskneves profile image
Vinicius Kiatkoski Neves

Nice! I'm glad it helped!