├── .gitignore ├── LICENSE ├── README.md ├── cloudformation └── sesEmailProcessor-cfn.json └── lambda-functions ├── storeSESBounce ├── app │ ├── Gruntfile.js │ └── package.json ├── deploy │ ├── environments │ │ ├── dev │ │ └── prod │ ├── lambda.json │ └── policy.lam.json └── src │ └── storeSESBounce_lambda.py └── storeSESDelivery ├── app ├── Gruntfile.js └── package.json ├── deploy ├── environments │ ├── dev │ └── prod ├── lambda.json └── policy.lam.json └── src └── storeSESDelivery_lambda.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Signiant Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-ses-recorder 2 | These lambda functions processes SES email deliveries and bounces and writes them to dynamoDB tables. There is also a [complementary GUI query tool](https://github.com/Signiant/aws-ses-recorder-query-tool) available . 3 | 4 | To install and use these: 5 | 6 | 1) Create a CloudFormation stack using the template here. This will create the required dynamoDB tables and SNS topics 7 | 8 | 2) Create the lambda functions using the [lambda-promotion](https://github.com/Signiant/lambda-promotion) tool. 9 | There are 2 functions - one handles bounces, one handles deliveries 10 | 11 | To deploy these functions : 12 | * run npm install in the app directory to install the build dependencies 13 | * run grunt to execute the build 14 | * execute the lambda-promotion tool, providing the absolute path to the dist directory (created by grunt), and prod as argument 15 | 16 | If you wish to deploy without the tool: 17 | * Create an iam role with the policy found in the deploy directory 18 | * Create new functions using the source code here, the role you created, and the configuration values specified in the deploy/environments/prod.lam.json file. 19 | * Configure each lambda function's event source mappings so that they are invoked by the corresponding SNS topic. 20 | -------------------------------------------------------------------------------- /cloudformation/sesEmailProcessor-cfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion" : "2010-09-09", 3 | "Resources" : { 4 | "bouncesDynamoDBTable" : { 5 | "Type" : "AWS::DynamoDB::Table", 6 | "Properties" : { 7 | "AttributeDefinitions" : [ 8 | { 9 | "AttributeName" : "sesMessageId", 10 | "AttributeType" : "S" 11 | }, 12 | { 13 | "AttributeName" : "recipientAddress", 14 | "AttributeType" : "S" 15 | }, 16 | { 17 | "AttributeName" : "sesTimestamp", 18 | "AttributeType" : "N" 19 | } 20 | ], 21 | "KeySchema" : [ 22 | { 23 | "AttributeName" : "recipientAddress", 24 | "KeyType" : "HASH" 25 | }, 26 | { 27 | "AttributeName" : "sesMessageId", 28 | "KeyType" : "RANGE" 29 | } 30 | ], 31 | "ProvisionedThroughput" : { 32 | "ReadCapacityUnits" : "5", 33 | "WriteCapacityUnits" : "5" 34 | }, 35 | "TableName" : "DEVOPS_SES_BOUNCES", 36 | "GlobalSecondaryIndexes" : [{ 37 | "IndexName" : "sesTimestamp-index", 38 | "KeySchema" : [ 39 | { 40 | "AttributeName" : "sesTimestamp", 41 | "KeyType" : "HASH" 42 | } 43 | ], 44 | "Projection" : { 45 | "ProjectionType" : "ALL" 46 | }, 47 | "ProvisionedThroughput" : { 48 | "ReadCapacityUnits" : "5", 49 | "WriteCapacityUnits" : "5" 50 | } 51 | }] 52 | } 53 | }, 54 | "deliveriesDynamoDBTable" : { 55 | "Type" : "AWS::DynamoDB::Table", 56 | "Properties" : { 57 | "AttributeDefinitions" : [ 58 | { 59 | "AttributeName" : "sesMessageId", 60 | "AttributeType" : "S" 61 | }, 62 | { 63 | "AttributeName" : "recipientAddress", 64 | "AttributeType" : "S" 65 | }, 66 | { 67 | "AttributeName" : "sesTimestamp", 68 | "AttributeType" : "N" 69 | } 70 | ], 71 | "KeySchema" : [ 72 | { 73 | "AttributeName" : "recipientAddress", 74 | "KeyType" : "HASH" 75 | }, 76 | { 77 | "AttributeName" : "sesMessageId", 78 | "KeyType" : "RANGE" 79 | } 80 | ], 81 | "ProvisionedThroughput" : { 82 | "ReadCapacityUnits" : "5", 83 | "WriteCapacityUnits" : "5" 84 | }, 85 | "TableName" : "DEVOPS_SES_DELIVERIES", 86 | "GlobalSecondaryIndexes" : [{ 87 | "IndexName" : "sesTimestamp-index", 88 | "KeySchema" : [ 89 | { 90 | "AttributeName" : "sesTimestamp", 91 | "KeyType" : "HASH" 92 | } 93 | ], 94 | "Projection" : { 95 | "ProjectionType" : "ALL" 96 | }, 97 | "ProvisionedThroughput" : { 98 | "ReadCapacityUnits" : "5", 99 | "WriteCapacityUnits" : "5" 100 | } 101 | }] 102 | } 103 | }, 104 | "bouncesSNSTopic": { 105 | "Type" : "AWS::SNS::Topic", 106 | "Properties" : { 107 | "DisplayName" : "SESBounce", 108 | "TopicName" : "SES_Email_Bounce_Notifications" 109 | } 110 | }, 111 | "deliveriesSNSTopic": { 112 | "Type" : "AWS::SNS::Topic", 113 | "Properties" : { 114 | "DisplayName" : "SESDeliv", 115 | "TopicName" : "SES_Email_Delivery_Notifications" 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/app/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt){ 2 | var pkg = grunt.file.readJSON('package.json'); 3 | 4 | grunt.initConfig({ 5 | compress: { 6 | main: { 7 | options: { 8 | archive: '../dist/lambda.zip', 9 | }, 10 | files: [ 11 | { 12 | src: 'storeSESBounce_lambda.py', 13 | cwd: '../src', 14 | expand: true 15 | } 16 | ] 17 | } 18 | }, 19 | copy: { 20 | main: { 21 | files: [ 22 | {expand: true, src: ['../deploy/policy.lam.json'], dest: '../dist/deploy/', filter: 'isFile'} 23 | ] 24 | } 25 | } 26 | }) 27 | 28 | grunt.registerTask('produce-deployment-rules', function(){ 29 | var propertiesReader = require('properties-reader'); 30 | var sourceFiles = grunt.file.expand("../deploy/environments/*"); 31 | sourceFiles.forEach(function(pathToSource){ 32 | var filePathArray = pathToSource.split("/"); 33 | var envName = filePathArray[filePathArray.length - 1].split(".")[0]; 34 | var envProperties = propertiesReader(pathToSource); 35 | 36 | grunt.file.copy("../deploy/lambda.json", '../dist/deploy/environments/' + envName + '.lam.json', { 37 | process: function(text) { 38 | text = replaceText(text, envProperties); 39 | return text; 40 | } 41 | }); 42 | }); 43 | }); 44 | 45 | function replaceText(text, envProperties){ 46 | var textWithReplacements = text; 47 | envProperties.each(function(key, value) { 48 | textWithReplacements = textWithReplacements.replace(new RegExp('##' + key.replace('.', '\.') + '##', 'g'), value); 49 | }); 50 | return textWithReplacements; 51 | } 52 | 53 | grunt.loadNpmTasks('grunt-contrib-compress'); 54 | grunt.loadNpmTasks('grunt-contrib-copy'); 55 | 56 | grunt.registerTask('default', ['compress', 'copy', 'produce-deployment-rules']); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store-ses-bounce", 3 | "version": "1.0.0", 4 | "description": "Writes an SES bounce to dynamoDB", 5 | "main": "Gruntfile.js", 6 | "devDependencies": { 7 | "grunt": "0.4.5", 8 | "grunt-contrib-compress": "^0.14.0", 9 | "grunt-contrib-copy": "0.8.2", 10 | "grunt-text-replace": "0.4.0", 11 | "properties-reader": "0.0.8" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/deploy/environments/dev: -------------------------------------------------------------------------------- 1 | application.aws.lambda.region=us-east-1 2 | 3 | application.aws.lambda.event.sns.topic=SES_Email_Bounce_Notifications 4 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/deploy/environments/prod: -------------------------------------------------------------------------------- 1 | application.aws.lambda.region=us-east-1 2 | 3 | application.aws.lambda.event.sns.topic=SES_Email_Bounce_Notifications 4 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/deploy/lambda.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeSESbounce", 3 | "archive": "lambda.zip", 4 | "description": "Writes an SES bounce to dynamoDB", 5 | "region": "us-east-1", 6 | "runtime": "python3.8", 7 | "memorySize": 128, 8 | "timeout": 180, 9 | "handler": "storeSESBounce_lambda.lambda_handler", 10 | "events": [ 11 | { 12 | "type": "sns", 13 | "src": "", 14 | "parameter": "SES_Email_Bounce_Notifications", 15 | "regions": ["us-east-1","us-west-2","eu-west-1"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/deploy/policy.lam.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "dynamo", 6 | "Action": [ 7 | "dynamodb:DeleteItem", 8 | "dynamodb:GetItem", 9 | "dynamodb:PutItem", 10 | "dynamodb:Query", 11 | "dynamodb:Scan", 12 | "dynamodb:UpdateItem" 13 | ], 14 | "Effect": "Allow", 15 | "Resource": "*" 16 | }, 17 | { 18 | "Sid": "", 19 | "Resource": "*", 20 | "Action": [ 21 | "logs:CreateLogGroup", 22 | "logs:CreateLogStream", 23 | "logs:PutLogEvents" 24 | ], 25 | "Effect": "Allow" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /lambda-functions/storeSESBounce/src/storeSESBounce_lambda.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import time 4 | import dateutil.parser 5 | import datetime 6 | import calendar 7 | 8 | 9 | # Helper class to convert a DynamoDB item to JSON. 10 | class DecimalEncoder(json.JSONEncoder): 11 | def default(self, o): 12 | if isinstance(o, decimal.Decimal): 13 | if o % 1 > 0: 14 | return float(o) 15 | else: 16 | return int(o) 17 | return super(DecimalEncoder, self).default(o) 18 | 19 | 20 | def lambda_handler(event, context): 21 | #print("Received event: " + json.dumps(event, indent=2)) 22 | 23 | processed = False 24 | DYNAMODB_TABLE = "DEVOPS_SES_BOUNCES" 25 | 26 | DDBtable = boto3.resource('dynamodb').Table(DYNAMODB_TABLE) 27 | 28 | # Generic SNS headers 29 | SnsMessageId = event['Records'][0]['Sns']['MessageId'] 30 | SnsPublishTime = event['Records'][0]['Sns']['Timestamp'] 31 | SnsTopicArn = event['Records'][0]['Sns']['TopicArn'] 32 | SnsMessage = event['Records'][0]['Sns']['Message'] 33 | 34 | print("Read SNS Message with ID " + SnsMessageId + " published at " + SnsPublishTime) 35 | 36 | now = time.strftime("%c") 37 | LambdaReceiveTime = now 38 | 39 | # SES specific fields 40 | SESjson = json.loads(SnsMessage) 41 | sesNotificationType = SESjson['notificationType'] 42 | 43 | if 'mail' in SESjson: 44 | sesMessageId = SESjson['mail']['messageId'] 45 | sesTimestamp = SESjson['mail']['timestamp'] #the time the original message was sent 46 | sender = SESjson['mail']['source'] 47 | 48 | print("Processing an SES " + sesNotificationType + " with mID " + sesMessageId) 49 | 50 | if (sesNotificationType == "Bounce"): 51 | print("Processing SES bounce messsage") 52 | 53 | try: 54 | reportingMTA = SESjson['bounce']['reportingMTA'] 55 | except: 56 | print("No reportingMTA provided in bounce notification") 57 | print("Received event: " + json.dumps(event, indent=2)) 58 | reportingMTA = "UNKNOWN" 59 | 60 | bounceType = SESjson['bounce']['bounceType'] 61 | bounceRecipients = SESjson['bounce']['bouncedRecipients'] 62 | bounceType = SESjson['bounce']['bounceType'] 63 | bounceSubType = SESjson['bounce']['bounceSubType'] 64 | bounceTimestamp = SESjson['bounce']['timestamp'] # the time at which the bounce was sent by the ISP 65 | 66 | # There can be a seperate bounce reason per recipient IF it's not a suppression bounce 67 | for recipient in bounceRecipients: 68 | try: 69 | recipientEmailAddress = recipient['emailAddress'] 70 | except: 71 | print("No recipient email provided in bounce notification") 72 | print("Received event: " + json.dumps(event, indent=2)) 73 | recipientEmailAddress = "UNKNOWN" 74 | 75 | try: 76 | diagnosticCode = recipient['diagnosticCode'] 77 | except: 78 | print("No diagnosticCode provided in bounce notification") 79 | print("Received event: " + json.dumps(event, indent=2)) 80 | diagnosticCode = "UNKNOWN" 81 | 82 | print("Bounced recipient: " + str(recipientEmailAddress) + " reason: " + str(diagnosticCode)) 83 | 84 | sesTimestamp_parsed = dateutil.parser.parse(sesTimestamp) 85 | sesTimestamp_seconds = sesTimestamp_parsed.strftime('%s') 86 | 87 | bounceTimestamp_parsed = dateutil.parser.parse(bounceTimestamp) 88 | bounceTimestamp_seconds = bounceTimestamp_parsed.strftime('%s') 89 | 90 | # Set an expiry time for this record so we can use Dynamo TTLs to remove 91 | future = datetime.datetime.utcnow() + datetime.timedelta(days=120) 92 | expiry_ttl = calendar.timegm(future.timetuple()) 93 | 94 | # Add entry to DB for this recipient 95 | Item={ 96 | 'recipientAddress': recipientEmailAddress, 97 | 'sesMessageId': sesMessageId, 98 | 'sesTimestamp': int(sesTimestamp_seconds), 99 | 'bounceTimestamp': int(bounceTimestamp_seconds), 100 | 'reportingMTA': reportingMTA, 101 | 'diagnosticCode': diagnosticCode, 102 | 'bounceType': bounceType, 103 | 'bounceSubType': bounceSubType, 104 | 'sender': sender.lower(), 105 | 'expiry': int(expiry_ttl) 106 | } 107 | 108 | response = DDBtable.put_item(Item=Item) 109 | print("PutItem succeeded:") 110 | print(json.dumps(response, indent=4, cls=DecimalEncoder)) 111 | 112 | processed = True 113 | 114 | else: 115 | print("Unhandled notification type: " + sesNotificationType) 116 | else: 117 | print("Incoming event is not a mail event") 118 | print("Received event was: " + json.dumps(event, indent=2)) 119 | processed = True 120 | 121 | return processed 122 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/app/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt){ 2 | var pkg = grunt.file.readJSON('package.json'); 3 | 4 | grunt.initConfig({ 5 | compress: { 6 | main: { 7 | options: { 8 | archive: '../dist/lambda.zip', 9 | }, 10 | files: [ 11 | { 12 | src: 'storeSESDelivery_lambda.py', 13 | cwd: '../src', 14 | expand: true 15 | } 16 | ] 17 | } 18 | }, 19 | copy: { 20 | main: { 21 | files: [ 22 | {expand: true, src: ['../deploy/policy.lam.json'], dest: '../dist/deploy/', filter: 'isFile'} 23 | ] 24 | } 25 | } 26 | }) 27 | 28 | grunt.registerTask('produce-deployment-rules', function(){ 29 | var propertiesReader = require('properties-reader'); 30 | var sourceFiles = grunt.file.expand("../deploy/environments/*"); 31 | sourceFiles.forEach(function(pathToSource){ 32 | var filePathArray = pathToSource.split("/"); 33 | var envName = filePathArray[filePathArray.length - 1].split(".")[0]; 34 | var envProperties = propertiesReader(pathToSource); 35 | 36 | grunt.file.copy("../deploy/lambda.json", '../dist/deploy/environments/' + envName + '.lam.json', { 37 | process: function(text) { 38 | text = replaceText(text, envProperties); 39 | return text; 40 | } 41 | }); 42 | }); 43 | }); 44 | 45 | function replaceText(text, envProperties){ 46 | var textWithReplacements = text; 47 | envProperties.each(function(key, value) { 48 | textWithReplacements = textWithReplacements.replace(new RegExp('##' + key.replace('.', '\.') + '##', 'g'), value); 49 | }); 50 | return textWithReplacements; 51 | } 52 | 53 | grunt.loadNpmTasks('grunt-contrib-compress'); 54 | grunt.loadNpmTasks('grunt-contrib-copy'); 55 | 56 | grunt.registerTask('default', ['compress', 'copy', 'produce-deployment-rules']); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store-ses-delivery", 3 | "version": "1.0.0", 4 | "description": "Writes an SES delivery to dynamoDB", 5 | "main": "Gruntfile.js", 6 | "devDependencies": { 7 | "grunt": "0.4.5", 8 | "grunt-text-replace": "0.4.0", 9 | "grunt-contrib-compress": "^0.14.0", 10 | "grunt-contrib-copy": "0.8.2", 11 | "properties-reader": "0.0.8" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/deploy/environments/dev: -------------------------------------------------------------------------------- 1 | application.aws.lambda.region=us-east-1 2 | 3 | application.aws.lambda.event.sns.topic=SES_Email_Delivery_Notifications 4 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/deploy/environments/prod: -------------------------------------------------------------------------------- 1 | application.aws.lambda.region=us-east-1 2 | 3 | 4 | application.aws.lambda.event.sns.topic=SES_Email_Delivery_Notifications 5 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/deploy/lambda.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeSESdelivery", 3 | "archive": "lambda.zip", 4 | "description": "Writes an SES delivery to dynamoDB", 5 | "region": "us-east-1", 6 | "runtime": "python3.8", 7 | "memorySize": 128, 8 | "timeout": 180, 9 | "handler": "storeSESDelivery_lambda.lambda_handler", 10 | "events": [ 11 | { 12 | "type": "sns", 13 | "src": "", 14 | "parameter": "SES_Email_Delivery_Notifications", 15 | "regions": ["us-east-1","us-west-2","eu-west-1"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/deploy/policy.lam.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "dynamo", 6 | "Action": [ 7 | "dynamodb:DeleteItem", 8 | "dynamodb:GetItem", 9 | "dynamodb:PutItem", 10 | "dynamodb:Query", 11 | "dynamodb:Scan", 12 | "dynamodb:UpdateItem" 13 | ], 14 | "Effect": "Allow", 15 | "Resource": "*" 16 | }, 17 | { 18 | "Sid": "", 19 | "Resource": "*", 20 | "Action": [ 21 | "logs:CreateLogGroup", 22 | "logs:CreateLogStream", 23 | "logs:PutLogEvents" 24 | ], 25 | "Effect": "Allow" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /lambda-functions/storeSESDelivery/src/storeSESDelivery_lambda.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import time 4 | import dateutil.parser 5 | import datetime 6 | import calendar 7 | 8 | 9 | # Helper class to convert a DynamoDB item to JSON. 10 | class DecimalEncoder(json.JSONEncoder): 11 | def default(self, o): 12 | if isinstance(o, decimal.Decimal): 13 | if o % 1 > 0: 14 | return float(o) 15 | else: 16 | return int(o) 17 | return super(DecimalEncoder, self).default(o) 18 | 19 | 20 | def lambda_handler(event, context): 21 | # print("Received event: " + json.dumps(event, indent=2)) 22 | 23 | processed = False 24 | DYNAMODB_TABLE = "DEVOPS_SES_DELIVERIES" 25 | 26 | DDBtable = boto3.resource('dynamodb').Table(DYNAMODB_TABLE) 27 | 28 | # Generic SNS headers 29 | SnsMessageId = event['Records'][0]['Sns']['MessageId'] 30 | SnsPublishTime = event['Records'][0]['Sns']['Timestamp'] 31 | SnsTopicArn = event['Records'][0]['Sns']['TopicArn'] 32 | SnsMessage = event['Records'][0]['Sns']['Message'] 33 | 34 | print("Read SNS Message with ID " + SnsMessageId + " published at " + SnsPublishTime) 35 | 36 | now = time.strftime("%c") 37 | LambdaReceiveTime = now 38 | 39 | # SES specific fields 40 | SESjson = json.loads(SnsMessage) 41 | sesNotificationType = SESjson['notificationType'] 42 | 43 | if 'mail' in SESjson: 44 | sesMessageId = SESjson['mail']['messageId'] 45 | sesTimestamp = SESjson['mail']['timestamp'] 46 | sender = SESjson['mail']['source'] 47 | 48 | print("Processing an SES " + sesNotificationType + " with mID " + sesMessageId) 49 | 50 | if sesNotificationType == "Delivery": 51 | print("Processing SES delivery message") 52 | 53 | reportingMTA = SESjson['delivery']['reportingMTA'] 54 | deliveryRecipients = SESjson['delivery']['recipients'] 55 | smtpResponse = SESjson['delivery']['smtpResponse'] 56 | deliveryTimestamp = SESjson['delivery']['timestamp'] 57 | processingTime = SESjson['delivery']['processingTimeMillis'] 58 | 59 | # there can be multiple recipients but the SMTPresponse is the same for each 60 | for recipient in deliveryRecipients: 61 | recipientEmailAddress = recipient 62 | 63 | print("Delivery recipient: " + recipientEmailAddress) 64 | 65 | sesTimestamp_parsed = dateutil.parser.parse(sesTimestamp) 66 | sesTimestamp_seconds = sesTimestamp_parsed.strftime('%s') 67 | 68 | deliveryTimestamp_parsed = dateutil.parser.parse(deliveryTimestamp) 69 | deliveryTimestamp_seconds = deliveryTimestamp_parsed.strftime('%s') 70 | 71 | # Set an expiry time for this record so we can use Dynamo TTLs to remove 72 | # 4 months but easy to change 73 | future = datetime.datetime.utcnow() + datetime.timedelta(days=120) 74 | expiry_ttl = calendar.timegm(future.timetuple()) 75 | 76 | # Add entry to DB for this recipient 77 | Item={ 78 | 'recipientAddress': recipientEmailAddress, 79 | 'sesMessageId': sesMessageId, 80 | 'sesTimestamp': int(sesTimestamp_seconds), 81 | 'deliveryTimestamp': int(deliveryTimestamp_seconds), 82 | 'processingTime': int(processingTime), 83 | 'reportingMTA': reportingMTA, 84 | 'smtpResponse': smtpResponse, 85 | 'sender': sender.lower(), 86 | 'expiry': int(expiry_ttl) 87 | } 88 | 89 | response = DDBtable.put_item(Item=Item) 90 | print("PutItem succeeded:") 91 | print(json.dumps(response, indent=4, cls=DecimalEncoder)) 92 | 93 | processed = True 94 | 95 | else: 96 | print("Unhandled notification type: " + sesNotificationType) 97 | else: 98 | print("Incoming event is not a mail event") 99 | print("Received event was: " + json.dumps(event, indent=2)) 100 | processed = True 101 | 102 | return processed 103 | --------------------------------------------------------------------------------