Code Monkey home page Code Monkey logo

aws-ses-lambda's Introduction

AWS SES Lambda ๐Ÿ“ฌ

An AWS Lambda Function that Sends Email via Simple Email Service (SES) and handles notifications for bounces, etc. ๐Ÿ“ˆ

Build Status codecov.io Code Climate maintainability dependencies Status HitCount npm package version Node.js Version


Why? ๐Ÿคท

We send (and receive) a lot of email both for the @dwyl App and our newsletter.
We need a simple, scalable & maintainable way of sending email, and most importantly we needed to know with certainty:

  • Are our emails being delivered?
  • How many emails are bouncing?
  • Are we attempting to re-send email to addresses that have "bounced"? (i.e. wasting money?!)
  • Are people opening / reading the email?
  • Do people engage with the content of the email? (click through)
  • If someone no longer wants to receive emails (too many or not relevant), do we have a reliable way for them to unsubscribe?

This project is our quest to answer these questions.

What? ๐Ÿ’ก

The aws-ses-lambda function does three related things1:

  1. Send emails.
  2. Parse AWS SNS notifications related to the emails that were sent.
  3. Save the parsed SNS notification data for aggregation and visualisation.

The How? section below explains how each of these functions works.

This diagram explains the context where aws-ses-lambda is used:

dwyl-app-services-diagram

Edit this diagram

The Email App receives requests from the Auth App and and triggers the aws-ses-lambda function.

The aws-ses-lambda function Sends email and handles SNS notifications for bounce events.

How?

As the name of this project suggests, we are using AWS Lambda, to handle all email-related tasks via AWS SES.

If you (or anyone else on your team) are new to AWS Lambda, see: github.com/dwyl/learn-aws-lambda

In this section we will break down how the lambda works.

1. Send Email

Thanks to the work we did earlier on sendemail, sending emails using AWS Simple Email Service (SES) from our Lambda function is very simple.

We just need to follow the setup instructions in github.com/dwyl/sendemail#how including creating a /templates directory, then create a handler function:

const sendemail = require('sendemail').email;

module.exports = function send (event, callback) {
  return sendemail(event.template, event, callback);
};

Don't you just love it when things are that simple?!
All the data required for sending an email is received in the Lambda event object.

The required keys in the event object are:

  • email - the email address we want to send an email to.
  • name - the name of the person we are sending the email to. (if your email messages aren't personal, don't send them!)
  • subject - the subject of the email you are sending.
  • template - the template you want to send.

It works flawlessly.

The full code is: lib/send.js

2. Parse AWS SNS Notifications

After an email is sent using AWS SES, AWS keeps track of the status of the emails e.g delivered, bounce or complaint.
By subscribing to AWS Simple Notification System (SNS) notifications, we can keep track of the status.

There are a few steps for setting up SNS notifications for SES events, so we created detailed setup instructions: SETUP.md

Once you have configured the SNS Topic, used the topic for SES notifications and set the topic as the trigger for the lambda function, it's time to parse the notifications.

Thankfully this is also really simple code!

let json = {};
if(event && event.Records && event.Records.length > 0) {
  const msg = JSON.parse(event.Records[0].Sns.Message);
  json.messageId = msg.mail.messageId;
  json.notificationType = msg.notificationType + ' ' + msg.bounce.bounceType;
}

We are only interested in the messageId and notificationType. This code is included in lib/parse.js

During MVP we are only interested in the emails that bounce. So we are only parsing the bounce event. Gmail does not send delivery notifications, so we will need to implement a workaround. See: dwyl/email#1

More detail on the various SES SNS notifications: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-examples.html

3. Save SNS Notification Data

Once we have parsed the SNS notifications for SES events, we need to save the data back to our PostgreSQL database so that we can build our analytics dashboard!

This again is pretty simple code; we just invoke http_request with the json data we want to send to the Phoenix App:

const json = parse(event); // parse SNS event see: step 2.
http_request(json, callback); // json data & lambda callback argument

View the complete code in index.js and the supporting http_request function in lib/http_request.js

The http_request function wraps the Node.js core http.request method with a few basic options and allows us to pass in a json Object to send to the Phoenix App.

Required Environment Variables

In order for all parts of the Lambda function to work, we need to ensure that all environment variables are defined.

For the complete list of required environment variables, please see the .env_sample file.

Copy the .env_sample file and create a .env file:

cp .env_sample .env

Then update all the values in the file so that they are the real values.

Once you have a .env file with all the correct environment variables, it's time to deploy the Lambda function to AWS!

Deploy the Lambda to AWS!

Run the following command in your terminal:

npm run deploy

You should see output similar to the following:

- - - - - - - - > Lambda Function Deployed:
{
  FunctionName: 'aws-ses-lambda-v1',
  FunctionArn: 'arn:aws:lambda:eu-west-1:123456789247:function:aws-ses-lambda-v1',
  Runtime: 'nodejs12.x',
  Role: 'arn:aws:iam::123456789247:role/service-role/LambdaExecRole',
  Handler: 'index.handler',
  CodeSize: 8091768,
  Description: 'A complete solution for sending email via AWS SES using Lambda',
  Timeout: 42,
  MemorySize: 128,
  LastModified: '2020-03-05T23:42:56.809+0000',
  CodeSha256: 'jvOg/+8y9UwBcLeTprMRIEvT0ryun1bdjzrAJXAk5m8=',
  Version: '$LATEST',
  Environment: { Variables: { EMAIL_APP_URL: 'phemail.herokuapp.com' } },
  TracingConfig: { Mode: 'PassThrough' },
  RevisionId: '42442cee-d506-4aa5-aec5-d7fb73145a58',
  State: 'Active',
  LastUpdateStatus: 'Successful'
}
- - - - - - - - > took 8.767 seconds

Ensure you follow all the instructions in SETUP.md to get the SNS Topic to trigger the Lambda function for SES notifications.

Debugging

Enable debugging by setting the NODE_ENV=test environment variable.

NODE_ENV=test

Now the latest event will be saved to: https://ademoapp.s3.eu-west-1.amazonaws.com/event.json

image

And SNS messages are saved to: https://ademoapp.s3.eu-west-1.amazonaws.com/sns.json

image




tl;dr

Extended Why?

There are way more reasons why we are handcrafting this app than the ones stated above.
We see email as our primary feedback mechanism and thus "operationally strategic", not merely "transactional". i.e. not something to be "outsourced" to a "black box" provider that "takes care of everything" for us. We want to have full control and deep insights into our email system.
By using a decoupled lambda function to send email and subscribe to SNS events we keep all the AWS specific functionality in a single place. This is easy to reason about, maintain and extend when required. In the future, if we decide to switch email sending provider, (or run our own email service), we can simply re-write the sendemail and parse_notification functions and not need to touch our email analytics dashboard at all!

For now SES is by far the cheapest and superbly reliable way to send email. We are very happy to let AWS take care of this part of our stack.

Why Only One Lambda Function?

1 The aws-ses-lambda function does 3 things because they relate to the unifying theme of sending email via SES and tracking the status of the sent emails via SNS. We could have split these 3 bits of functionality into separate repositories and deploy them separately as distinct lambda functions, however in our experience having too many lambda functions can quickly become a maintenance headache. We chose to group them together because they are small, easy to reason about and work well as a team! If you feel strongly about the UNIX Philosophy definitely split out the functions in your own fork/implementation.
The code for this Lambda function is less than 100 lines and can be read in 10 minutes. The sendemail module which the Lambda uses to send emails via AWS SES is 38 lines of code. See: lib/index.js it's mostly comments which make it very beginner friendly.

aws-ses-lambda's People

Contributors

dependabot[bot] avatar nelsonic avatar roryc89 avatar simonlab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

aws-ses-lambda's Issues

Setup IAM Role to Execute Lambda and Send Email via SES

In order to deploy and invoke a Lambda function and send email from that function,
we need to define an AWS Identity and Access Management (IAM) Role with the required permissions.

Todo

  • Take screenshots of all steps in the process while setting up an IAM Role
  • name: lambda-user
  • Permissions:
    • AWSLambdaFullAccess
    • AmazonSESFullAccess
    • AmazonS3FullAccess
  • No tags.

Original Features / Requirements

These are the steps we originally defined:


Each time an email is sent using the aws-ses-lambda 5 things happen:

1. Pre-Send Checks

  • Is the email address valid. (basic checks to avoid wasting money)
  • Check our records to see if the email address we are attempting to
    send to has bounced in the past.

2. Send Email

  • Send the email using AWS SES and keep a note of the
    unique ID confirming the email was sent.

3. Log Email Sent

  • Log all detail of the sent email to the Database

4. Query SES Bounce/Spam Info Service

  • Check which emails have bounced

5. Report Stats/Summary in Dashboard

  • Update the Database with the send/receipt stats
    so that they can be viewed on the team's Email Dashboard!

Frequently Asked Questions (FAQ)

Separate Lambda Functions or One Lambda with Independent Functions?

From experience making distinct functions separate lambda functions,
just increases our cost/complexity without any discernible benefit.

Having separate lambdas when we know that all functionality is executed
each time

aws-ses-bounce-checker periodically retrieves stats on outbound emails from SES.
These include bounce rates and whether an email has been opened.


We need to take a fresh look at these and determine which features are still needed.

Potential Future Features

This is a potential features "roadmap" in (descending) order of importance.

  • Email templates should be in a separate repo
    from the "main" application in any (all) our projects.
    Non-technical team members should be able
    to update the template (using their "basic" HTML + handlebars knowledge)
    and should be able to save a draft of the changes to GitHub
    (without having to run the "CI" for an entire "web app" project!)
  • Send Sample Email to myself to check layout/design and confirm
    the flow is working.
  • Extend the Deployment Tool
    to automatically configure the API Gateway

    see: dwyl/learn-aws-lambda#62 (comment)
    (for now I am doing it manually...)
    see: https://github.com/dwyl/aws-lambda-deploy
  • Consider Using DynamoDB for truly "Serverless" email?
    (this could/should be implemented by someone other than the "core" dwyl team
    as I have no "appetite" for the DynamoDB pricing model... but it's something
    someone else might consider adding
    )

    Not using DynamoDB. It has a horrible query API. Using PostgreSQL instead!
    see: https://github.com/dwyl/email/

If you are keen on a particular feature or want to propose other ideas, please comment below! ๐Ÿ‘

Host: https. is not in the cert's altnames: DNS:*.herokuapp.com, DNS:herokuapp.com

While attempting to send SNS notification data to the Phoenix App https://phemail.herokuapp.com
we are seeing the following error:

problem with request: Hostname/IP does not match certificate's altnames: Host: https. is not in the cert's altnames: DNS:*.herokuapp.com, DNS:herokuapp.com

image

Reading request/request#1777 we discover
that we have the option to ignore this error by adding the option:

rejectUnauthorized: false,

https://stackoverflow.com/questions/36588963/node-js-requests-with-rejectunauthorized-as-false

Save Event Data to S3 for Debugging

In order to debug the data we are receiving in the Lambda event,
we need to save a copy of the event to S3 so that we can view it after the Lambda has finished execution.
(yes, we can do this by hunting through the cloudwatch logs, but it's so much easier if we can just open a file and view it immediately rather than scrolling for ages)

Todo

  • Save event object as JSON in a file on S3.

AccessDenied! (attempting to send email on Lambda)

Attempting to run the Lambda function on Lambda getting the following error:

{
  "errorType": "AccessDenied",
  "errorMessage": "User `arn:aws:sts::123456789:assumed-role/LambdaExecRole/aws-ses-lambda-v1' is not authorized to perform `ses:SendEmail' on resource `arn:aws:ses:eu-west-1:123456789:identity/[email protected]'",
  "trace": [
    "AccessDenied: User `arn:aws:sts::123456789:assumed-role/LambdaExecRole/aws-ses-lambda-v1' is not authorized to perform `ses:SendEmail' on resource `arn:aws:ses:eu-west-1:123456789:identity/[email protected]'",
    "    at Request.extractError (/var/task/node_modules/aws-sdk/lib/protocol/query.js:50:29)",
    "    at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:106:20)",
    "    at Request.emit (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:78:10)",
    "    at Request.emit (/var/task/node_modules/aws-sdk/lib/request.js:683:14)",
    "    at Request.transition (/var/task/node_modules/aws-sdk/lib/request.js:22:10)",
    "    at AcceptorStateMachine.runTo (/var/task/node_modules/aws-sdk/lib/state_machine.js:14:12)",
    "    at /var/task/node_modules/aws-sdk/lib/state_machine.js:26:10",
    "    at Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:38:9)",
    "    at Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:685:12)",
    "    at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:116:18)"
  ]
}

Investigating now ... ๐Ÿ”

Create "ping" (no operation) execution branch to prime lambda

One of the only "down sides" of AWS Lambda is latency.
It can take up to 700ms for a "cold" Lambda function to startup.
The median cold-start is around 400ms
Once the Lambda has been invoked and is cached, it only takes 50ms to respond to subsequent requests.
What this means in practice is that someone registering for our App they will wait up to 1 second for an email to be sent.

Todo

Invoke AWS Lambda Function from Elixir

There are a couple of options for invoking a Lambda function from Elixir:

Todo

Create Diagram explaining relationship between Email App and aws-ses-lambda

while building the functionality for aws-ses-lambda I've been working off my imagination ... ๐Ÿ’ก
Obviously that doesn't make it very easy for other people to understand it. ๐Ÿ™„

Todo

  • Make a nice version of this:
The main App does not do anything with Email as that is is not it's core function.
It delegates all email sending and monitoring activity to the aws-ses-lambda.

โ”Œโ”€โ”€โ”€โ”€โ”€โ” send  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
| App | ----->| aws-ses-lambda |โ” the Lambda function Sends email 
โ””โ”€โ”€โ”€โ”€โ”€โ”˜ email โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜| and handles SNS notifications
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    SNS Notification | for bounce events.
  | Email | <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  | Stats |
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  The Email Stats App aggregates and visualises email stats.
             This allows us to be more data-driven in our communications.
             And understand exactly who is engaged with the app.

Pricing of mails originating from Lambda functions

Hi,

I was just wondering how AWS is invoicing SES mails sent from Lambda functions?
SES has two types of invoices:

  1. If the mail is originating from EC2 instances the first 62.000 mails are free
  2. In other cases you pay $0.10 for a 1000 mails
    So, under which of the 2 cases fall mails send from Lambda functions

Regards

Paul

Create Phoenix Endpoint to Receive SNS Notification Data

We have the Lambda function to send emails working. ๐ŸŽ‰
Now we want to create an SNS topic so that our Lambda can receive SES related notifications.
http://docs.aws.amazon.com/ses/latest/DeveloperGuide/configure-sns-notifications.html

But what happens when with the information in the SNS Notification?
In the past we have stored this data in S3 or DynamoDB but both those data stores are cumbersome to query and have per-request charges for saving/getting data. So we need a way of getting the SNS notification info back to our PostgreSQL (main) database.

Our ultimate objective with email is to have a real-time dashboard where we can visualise the aggregate deliverability stats and specific stats for a given person using our App (while preserving their anonymity/privacy).

We still want our Lambda function to receive and process the SNS messages,
and then forward the formatted info back to Phoenix App via REST API endpoint.

Todo

In order to use the same tables/schema as the MVP which has detailed explanations
see: https://github.com/dwyl/app-mvp-phoenix#schema
We are borrowing the actual migration files:

image

This way we can run the Email and Auth Apps in several ways:

  1. both apps use the same database (our preferred option during the first 100 people "Beta" phase)
  2. Auth and Email can be run separately
  3. Email can be run as an Umbrella App to Auth
  4. We can safely combine/merge all the apps into one App for simplicity.

Next

  • Create sent table/schema with the following fields:
    • person_id (references person.id) - the person the email was sent to.
    • template - the template that was used to send the email to the person.
    • message_id and request_id fields to store SES metadata.
    • status_id (references status.id) - the status of the email e.g: 6:sent, 7:delivered, 8:bounce, etc. This allows us to have unlimited statuses without tons of duplication of text.

    Note: the "status" does not need to be a single word, it can be for example:
    "bounce: addressee unknown"

Then

  • Create /api endpoint that:
    • requires a valid JWT in order to allow data in
    • receives data in a standardised form

Why is this needed?

AWS SES is great for sending email but does not have great UI for checking the stats of our sent emails. Thankfully, there's an API we can use to lookup the data and display it to our "admin" people so they are aware of the success/failure (bounce) rate. ๐Ÿ‘

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.