AWS - Using Serverless and Stripo to create and send emails with SES

Do you want to use the awesome Stripo service to create HTML email templates, and then send them with AWS SES and Lambda? Well, you are in the right place!

AWS - Using Serverless and Stripo to create and send emails with SES

The Plan

Using stripo.email we can create ourselves some awesome looking HTML email templates, and then using Serverless we can spin up an email service that lets us send those emails using AWS SES, invoked via a Lambda function.

What you need:

  • An AWS account
  • A domain name registered with AWS Route53

What we will do:

  1. Download this repository from my github account
  2. yarn install to install the dependencies
  3. Rename the .env.example file to .env and fill in the values
  4. sls deploy to deploy the service to AWS

Important note:

If you already have an SES identity for our domain in the region you are deploying this too, it will fail to deploy because the domain already exists in SES.

If this is the case, you will need to simply comment out everything under the resources: section in the serverless.yml file, and then deploy the service. This will then create the Lambda function & using your existing SES domain (its essentially the same thing, we are just skipping the SES setup part!)

The deployment serverless.yml script:

# The editable parts, either edit the below directly, or copy .env.example to .env and edit there!
custom:
  IAM_PROFILE: ${env:IAM_PROFILE} # e.g. YourIAMCredentialsUser
  SERVICE_NAME: ${env:SERVICE_NAME} # e.g. send-email-ses
  MY_REGION: ${env:MY_REGION} # e.g us-east-1
  EMAIL_DOMAIN: ${env:EMAIL_DOMAIN} # e.g yourdomain.com, must be registered in route53

  # NOTE: Only fill out the below if you know what you're doing!

  # Make sure we include any handlebars .hbs files
  bundle:
    rawFileExtensions:
      - hbs

service: ${self:custom.SERVICE_NAME}
useDotenv: true

provider:
  name: aws
  profile: ${self:custom.IAM_PROFILE}
  runtime: nodejs18.x
  region: ${self:custom.MY_REGION}
  iam:
    role:
      statements:
        - Effect: "Allow"
          Action:
            - ses:SendEmail
          Resource: "*"

plugins:
  - serverless-bundle # TypeScript bundler

functions:
  sendEmail:
    name: ${self:custom.SERVICE_NAME}-${sls:stage}
    environment:
      EMAIL_DOMAIN: ${self:custom.EMAIL_DOMAIN}
      REGION: ${self:custom.MY_REGION}
    handler: src/sendEmail.handler

# Comment out all of the below if you already have an email domain registered in SES
resources:
  Resources:
    SESIdentity:
      Type: AWS::SES::EmailIdentity
      Properties:
        DkimAttributes:
          SigningEnabled: true
        DkimSigningAttributes:
          NextSigningKeyLength: RSA_2048_BIT
        EmailIdentity: ${self:custom.EMAIL_DOMAIN}
        FeedbackAttributes:
          EmailForwardingEnabled: true
        MailFromAttributes:
          BehaviorOnMxFailure: USE_DEFAULT_VALUE
          MailFromDomain: email.${self:custom.EMAIL_DOMAIN}
    Route53SESIdendityVerificationRecord1:
      Type: AWS::Route53::RecordSet
      Properties:
        Name: !GetAtt SESIdentity.DkimDNSTokenName1
        Comment: ${self:custom.EMAIL_DOMAIN}-SES-1
        Type: CNAME
        HostedZoneName: ${self:custom.EMAIL_DOMAIN}.
        TTL: "900"
        ResourceRecords:
          - !GetAtt SESIdentity.DkimDNSTokenValue1
    Route53SESIdendityVerificationRecord2:
      Type: AWS::Route53::RecordSet
      Properties:
        Name: !GetAtt SESIdentity.DkimDNSTokenName2
        Comment: ${self:custom.EMAIL_DOMAIN}-SES-2
        Type: CNAME
        HostedZoneName: ${self:custom.EMAIL_DOMAIN}.
        TTL: "900"
        ResourceRecords:
          - !GetAtt SESIdentity.DkimDNSTokenValue2
    Route53SESIdendityVerificationRecord3:
      Type: AWS::Route53::RecordSet
      Properties:
        Name: !GetAtt SESIdentity.DkimDNSTokenName3
        Comment: ${self:custom.EMAIL_DOMAIN}-SES-3
        Type: CNAME
        HostedZoneName: ${self:custom.EMAIL_DOMAIN}.
        TTL: "900"
        ResourceRecords:
          - !GetAtt SESIdentity.DkimDNSTokenValue3
    Route53SESMailFromMX:
      Type: AWS::Route53::RecordSet
      Properties:
        Name: email.${self:custom.EMAIL_DOMAIN}
        Comment: ${self:custom.EMAIL_DOMAIN}-SES-MX
        Type: MX
        HostedZoneName: ${self:custom.EMAIL_DOMAIN}.
        TTL: "900"
        ResourceRecords:
          - 10 feedback-smtp.${self:custom.MY_REGION}.amazonses.com
    Route53SESMailFromTXT:
      Type: AWS::Route53::RecordSet
      Properties:
        Name: email.${self:custom.EMAIL_DOMAIN}
        Comment: ${self:custom.EMAIL_DOMAIN}-SES-TXT
        Type: TXT
        HostedZoneName: ${self:custom.EMAIL_DOMAIN}.
        TTL: "900"
        ResourceRecords:
          - '"v=spf1 include:amazonses.com ~all"'

The Lambda function

// DEPLOY:    sls deploy -f sendEmail --verbose
// LOGS:      sls logs -f sendEmail  -t
// BOTH:      sls deploy -f sendEmail --verbose && sls logs -f sendEmail  -t

import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import Handlebars from "handlebars";
import template from "./template.hbs";
const { REGION, EMAIL_DOMAIN } = process.env;
if (!EMAIL_DOMAIN) {
  throw new Error("EMAIL_DOMAIN is required");
}
if (!REGION) {
  throw new Error("REGION is required");
}

// Our expected inputs
interface iEvent {
  name: string;
  email: string;
  subject: string;
}

export const handler = async (event: iEvent) => {

  try {
    const sesClient = new SESClient({ region: REGION });
    const { name, email, subject } = event;

    // Compile HTML with Handlebars
    const compiled = Handlebars.compile(template);
    const htmlMessage = compiled({ username: name, useremail: email });

    // Set our source and replyTo addresses
    const source = `hello@${EMAIL_DOMAIN}`;
    const replyTo = `contact@${EMAIL_DOMAIN}`;

    const command = new SendEmailCommand({
      Source: source,
      Destination: {
        ToAddresses: [email], // Please note: if in Sandbox mode, this must be a verified email address
      },
      Message: {
        Subject: { Data: subject },
        Body: { 
          Html: {
            Charset: "UTF-8",
            Data: htmlMessage,
          },
          Text: {
            Charset: "UTF-8",
            Data: "Please view this email in a client that supports HTML",
          },
        },
      },
      ReplyToAddresses: [replyTo],
    });
    await sesClient.send(command);
    // You should have been sent the email by now!!

  } catch (error: any) {
    console.log("โŒ Error sending email:", error);
  }
};

The HTML/Handlerbars template

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<h1 style="color:red">{{username}}</h1>
<p style="color:green">Welcome {{username}}, {{useremail}} to mywebsite.com</p>

๐Ÿ‘† This HTML email has been kept as basic as possible for the purposes of this article/demo - but as you can see we can apply HTML formatting, and pass in variables to it (which get processed via the Handlebars module in our lambda function).

So you can start from here from scratch and build out your own amazing HTML email templates!

Testing our Lambda function

First either make sure your SES account is out of Sandbox mode, or that the email address that you are sending too has been verified in your AWS console.

Verifying our email address with AWS SES Console

Then go to Lambda in the region you deployed this too, find the function called send-email-ses-dev and click on it (or to whatever you changed the service name to)

Click on the Test tab, and create a new test event.

{
  "name": "Name of User",
  "email": "yourverifiedemail@example.com",
  "subject": "This is a test email! I hope you like it! ๐Ÿฅณ"
}

Click Test, and you should receive the email! ๐ŸŽ‰

If you see any errors, read them carefully to see what went wrong and you can debug from there.

What about Stripo Email?

Well, I'm glad you asked! I also suddenly realized that halfway through this blog article, I might not have the rights to publish one of Stripo's HTML templates, so I will simply show you an example screenshot of what I managed to conjure up using their templates.

This is a design I will be implementing soon on my www.3dnames.co website once my next update is complete!

Template made with stripo.emil website

Variables used in this example template would be:

{
	"name": "Name of User",
	"email": "user@example.com",
	"subject": "Your Sign Up Code",
	"code" : "123456",
	"website" : "www.3dnames.co"
}