Skip to content

Commit

Permalink
feat(OY2-27029 & OY2-27030): Implement Basic Email Sending Functional…
Browse files Browse the repository at this point in the history
…ity & Implement Retry Mechanism for Failed Emails (#348)
  • Loading branch information
kristin-at-theta authored Jan 31, 2024
1 parent 85ecb27 commit a0381ec
Show file tree
Hide file tree
Showing 9 changed files with 1,235 additions and 11 deletions.
4 changes: 2 additions & 2 deletions docs/assets/diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/docs/services/email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
layout: default
title: email
parent: Services
---

# data
{: .no_toc }

## Summary
The email service deploys the lambdas, SNS topics, and Configuration Sets needed to send email.

## Detail
AWS SES is an account-wide service for basic sending and receiving of email. By creating lambdas to build the emails and sending the email with a branch-specific configuration set, we can follow the events of email sending and take action based on those events.
2 changes: 2 additions & 0 deletions serverless-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ services:
path: src/services/data
params:
ECSFailureTopicArn: ${alerts.ECSFailureTopicArn}
email:
path: src/services/email
uploads:
path: src/services/uploads
ui-infra:
Expand Down
5 changes: 5 additions & 0 deletions src/services/email/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
root: true,
extends: ["custom-server"],
};

15 changes: 15 additions & 0 deletions src/services/email/handlers/processEmailEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const main = async (event, context, callback) => {
console.log(
"Received email event, stringified:",
JSON.stringify(event, null, 4)
);

let message;
if (typeof event.Records[0].Sns.Message === "string")
message = { "simpleMessage": event.Records[0].Sns.Message };
else
message = JSON.parse(event.Records[0].Sns.Message);
console.log("Message received from SNS:", message);

callback(null, "Success");
};
44 changes: 44 additions & 0 deletions src/services/email/handlers/processEmails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";

const createSendEmailCommand = (event) =>
new SendEmailCommand({
Source: "[email protected]",
Destination: {
ToAddresses: [
"[email protected]",
],
},
Message: {
Subject: {
Data: event.subject ?? "Subject Required",
Charset: "UTF-8",
},
Body: {
Text: {
Data: "Body Text",
Charset: "UTF-8",
},
Html: {
Data: "<p>HTML body text</p><p>yup</p>",
Charset: "UTF-8",
},
},
},
ConfigurationSetName: process.env.emailConfigSet,
});

const SES = new SESClient({ region: process.env.region });

export const main = async (event, context, callback) => {
let response;
console.log("Received event (stringified):", JSON.stringify(event, null, 4));
const sendEmailCommand = createSendEmailCommand(event);

try {
response = await SES.send(sendEmailCommand);
console.log("sendEmailCommand response: ", response);
} catch (err) {
console.log("Failed to process emails.", err);
}
callback(null, "Success");
};
21 changes: 21 additions & 0 deletions src/services/email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "email",
"description": "",
"private": true,
"version": "0.0.0",
"main": "index.js",
"author": "",
"license": "CC0-1.0",
"dependencies": {
"@aws-sdk/client-ses": "^3.499.0"
},
"scripts": {
"lint": "eslint '**/*.{ts,js}'"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2"
}
}
171 changes: 171 additions & 0 deletions src/services/email/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
service: ${self:custom.project}-email

frameworkVersion: "3"

plugins:
- serverless-stack-termination-protection
- "@stratiformdigital/serverless-s3-security-helper"
- "@stratiformdigital/serverless-iam-helper"
- serverless-plugin-scripts
- serverless-esbuild

provider:
name: aws
runtime: nodejs18.x
region: us-east-1
iam:
role:
path: /delegatedadmin/developer/
permissionsBoundary: arn:aws:iam::${aws:accountId}:policy/cms-cloud-admin/developer-boundary-policy
statements:
- Effect: Allow
Action:
- sts:AssumeRole
Resource: "*"
- Effect: Allow
Action:
- ses:ListIdentities
- ses:ListConfigurationSets
- ses:SendEmail
Resource: "*"
- Effect: Allow
Action:
- sns:Subscribe
- sns:Publish
Resource: "*"

stackTags:
PROJECT: ${self:custom.project}
SERVICE: ${self:service}

custom:
project: ${env:PROJECT}
emailEventTopicName: ${self:service}-${sls:stage}-email-events
serverlessTerminationProtection:
stages: # Apply CloudFormation termination protection for these stages
- master
- val
- production

functions:
processEmails:
handler: handlers/processEmails.main
environment:
region: ${self:provider.region}
emailConfigSet: ${self:service}-${sls:stage}-configuration
maximumRetryAttempts: 0
timeout: 60
memorySize: 1024
processEmailEvents:
handler: handlers/processEmailEvents.main
events:
- sns:
arn: !Ref EmailEventTopic
topicName: ${self:custom.emailEventTopicName}

resources:
Resources:
EmailEventTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:custom.emailEventTopicName}
DisplayName: Monitoring the sending of emails
KmsMasterKeyId: !Ref KmsKeyForEmails

KmsKeyForEmails:
Type: AWS::KMS::Key
Properties:
EnableKeyRotation: "true"
KeyPolicy:
Version: "2012-10-17"
Statement:
- Sid: Allow access for Root User
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
- Sid: Allow access for Key User (SNS Service Principal)
Effect: Allow
Principal:
Service: "sns.amazonaws.com"
Action:
- "kms:GenerateDataKey"
- "kms:Decrypt"
Resource: "*"
- Sid: Allow CloudWatch events to use the key
Effect: Allow
Principal:
Service: events.amazonaws.com
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey"
Resource: "*"
- Sid: Allow CloudWatch for CMK
Effect: Allow
Principal:
Service:
- cloudwatch.amazonaws.com
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey*"
Resource: "*"
- Sid: Allow SES events to use the key
Effect: Allow
Principal:
Service:
- ses.amazonaws.com
Action:
- "kms:Decrypt"
- "kms:GenerateDataKey*"
Resource: "*"

EmailEventTopicPolicy:
Type: AWS::SNS::TopicPolicy
Properties:
PolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- ses.amazonaws.com
Action:
- sns:Subscribe
- sns:Publish
Resource: !Ref EmailEventTopic
Topics:
- !Ref EmailEventTopic

EmailEventSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref EmailEventTopic
Endpoint: !GetAtt ProcessEmailEventsLambdaFunction.Arn
Protocol: lambda

EmailEventConfigurationSet:
Type: AWS::SES::ConfigurationSet
Properties:
Name: "${self:service}-${sls:stage}-configuration"

EmailEventConfigurationSetEventDestination:
Type: AWS::SES::ConfigurationSetEventDestination
Properties:
ConfigurationSetName: !Ref EmailEventConfigurationSet
EventDestination:
Enabled: true
Name: "${self:service}-${sls:stage}-destination"
MatchingEventTypes:
- "send"
- "reject"
- "bounce"
- "complaint"
- "delivery"
- "open"
- "click"
- "renderingFailure"
- "deliveryDelay"
- "subscription"
SnsDestination:
TopicARN: !Ref EmailEventTopic
Loading

0 comments on commit a0381ec

Please sign in to comment.