How do I use Lambda to store Amazon SNS notifications for Amazon SES emails in DynamoDB?

8 minute read
1

I use Amazon Simple Notification Service (Amazon SNS) to receive notifications about emails sent through Amazon Simple Email Service (Amazon SES). I want to set up an AWS Lambda function to store these notifications in an Amazon DynamoDB table.

Resolution

Note: The code examples in the following steps apply to a Lambda Node.js runtimes with V3 SDK, such as Node.js 18.x and 20.x.

Prerequisite: Set up an Amazon SES email or domain with an Amazon SNS topic that's configured to receive notifications from Amazon SES. For more information, see Receiving Amazon SES notifications using Amazon SNS.

Create a DynamoDB table

Create a table in DynamoDB that has the following attributes:

  • For the Table-name, enter SESNotifications.
  • For the primary Partition key, enter SESMessageId.
  • For the primary Sort key, enter SnsPublishTime.

To allow Lambda to query the table and create an Amazon SES report, set up the following secondary indexes:

Index namePartition keySort key
SESMessageType-IndexSESMessageType (String)SnsPublishTime (String)
SESMessageComplaintType-IndexSESComplaintFeedbackType (String)SnsPublishTime (String)

Note: You can add more secondary indexes as needed.

For information, see Create a DynamoDB table.

Add permissions to your Lambda function's IAM role that allow it to call the DynamoDB table

Create a new AWS Identity and Access Management (IAM) role. Configure the role to allow your Lambda function to call the DynamoDB:PutItem API.

Note: It's a best practice to create and use a new IAM role for different Lambda functions. Don't reuse roles across multiple functions.

To add permissions to your Lambda function's IAM role, complete the following steps:

  1. Open the IAM console.
  2. In the navigation pane, choose Roles.
  3. Choose Create Role.
  4. For Select type of trusted entity, choose AWS service.
  5. For Choose a use case, choose Lambda. Then, choose Next: Permissions.
  6. For Attach permissions policies, select the check box next to the AWSLambdaBasicExecutionRole managed policy. Then, choose Next: Tags.
  7. (Optional) Add IAM tags to the role. For more information, see Tagging IAM resources.
  8. Choose Next: Review.
  9. For Role name*, enter lambda_ses_execution.
  10. Choose Create role.
  11. Return IAM Roles, and then choose the role.
  12. On the Permissions tab, choose Add inline policy.
  13. On the Visual editor tab, choose Choose a service.
  14. Choose DynamoDB.
  15. In the Actions search field, enter PutItem. In the dropdown list, select the check box next to PutItem.
  16. For Resources, choose Specific.
  17. Choose Add ARN. Then, in the text box, enter your DynamoDB table's ARN.
  18. Choose Review policy.
  19. For Name, enter a name for the policy.
  20. Choose Create policy.

Example inline IAM policy that grants access to a DynamoDB table:

{
    "Version": "2012-10-17",
    "Statement": [
         {
            "Sid": "Stmt1428510662000",
            "Effect": "Allow",
            "Action": [
                "DynamoDB:PutItem"
            ],
            "Resource": [
                "arn:aws:DynamoDB:us-east-1:12345678912:table/SESNotifications"
            ]
        }
    ]
}

Create a Lambda function to process Amazon SES and Amazon SNS notifications

Use the following example function code to create a Lambda function that's named sesnotificationscode. You can use the following example Lambda function as a template to write data to a customer relationship management (CRM) system or other destinations.

Important: Assign the lambda_ses_execution role to the function.

console.log("Loading event");

import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});

export const handler = async (event) => {
  console.log("Received event:", JSON.stringify(event, null, 2));

  var SnsPublishTime = event.Records[0].Sns.Timestamp;
  var SnsTopicArn = event.Records[0].Sns.TopicArn;
  var SESMessage = event.Records[0].Sns.Message;

  SESMessage = JSON.parse(SESMessage);

  var SESMessageType = SESMessage.notificationType;
  var SESMessageId = SESMessage.mail.messageId;
  var SESDestinationAddress = SESMessage.mail.destination.toString();
  var LambdaReceiveTime = new Date().toString();

  if (SESMessageType == "Bounce") {
    var SESreportingMTA = SESMessage.bounce.reportingMTA;
    var SESbounceSummary = JSON.stringify(SESMessage.bounce.bouncedRecipients);
    var itemParams = {
      TableName: "SESNotifications",
      Item: {
        SESMessageId: { S: SESMessageId },
        SnsPublishTime: { S: SnsPublishTime },
        SESreportingMTA: { S: SESreportingMTA },
        SESDestinationAddress: { S: SESDestinationAddress },
        SESbounceSummary: { S: SESbounceSummary },
        SESMessageType: { S: SESMessageType },
      },
    };
    const command = new PutItemCommand(itemParams);
    try {
    const response = await client.send(command);
    console.log("Put Item Response Bounce: ", response);
    } catch (err) {
     console.log("Error", err);
    }  
    
  } else if (SESMessageType == "Delivery") {
    var SESsmtpResponse1 = SESMessage.delivery.smtpResponse;
    var SESreportingMTA1 = SESMessage.delivery.reportingMTA;
    var itemParamsdel = {
      TableName: "SESNotifications",
      Item: {
        SESMessageId: { S: SESMessageId },
        SnsPublishTime: { S: SnsPublishTime },
        SESsmtpResponse: { S: SESsmtpResponse1 },
        SESreportingMTA: { S: SESreportingMTA1 },
        SESDestinationAddress: { S: SESDestinationAddress },
        SESMessageType: { S: SESMessageType },
      },
    };
    const commanddel = new PutItemCommand(itemParamsdel);
    try {
    const responsedel = await client.send(commanddel);
    console.log("Put Item Response Delivery: ", responsedel);
    } catch (err) {
     console.log("Error", err);
    } 
  } else if (SESMessageType == "Complaint") {
    var SESComplaintFeedbackType = SESMessage.complaint.complaintFeedbackType;
    var SESFeedbackId = SESMessage.complaint.feedbackId;
    var itemParamscomp = {
      TableName: "SESNotifications",
      Item: {
        SESMessageId: { S: SESMessageId },
        SnsPublishTime: { S: SnsPublishTime },
        SESComplaintFeedbackType: { S: SESComplaintFeedbackType },
        SESFeedbackId: { S: SESFeedbackId },
        SESDestinationAddress: { S: SESDestinationAddress },
        SESMessageType: { S: SESMessageType },
      },
    };
    const commandcomp = new PutItemCommand(itemParamscomp);
    try {
    const responsecomp = await client.send(commandcomp);
    console.log("Put Item Response complaint: ", responsecomp);
    } catch (err) {
     console.log("Error", err);
    } 
  }
};

Note: Replace the TableName parameter, SESNotifications, with your DynamoDB table's name.

Subscribe your Lambda function to one or more Amazon SNS topics

You can use the Amazon SNS console or the Lambda console to subscribe your Lambda function to one or more Amazon SNS topics.

Amazon SNS console

Note: If you receive errors when you run AWS Command Line Interface (AWS CLI) commands, then see Troubleshoot AWS CLI errors. Also, make sure that you're using the most recent AWS CLI version.
You must manually add permissions to the function resource policy to allow SNS to invoke the function. Run the add-permission AWS CLI command:

aws lambda add-permission --function-name my-function --action lambda:InvokeFunction --statement-id sns-my-topic \\  \--principal sns.amazonaws.com --source-arn arn:aws:sns:us-east-1:123456789012:my-topic

Note: Replace my-function, sns-my-topic, and arn:aws:sns:us-east-1:123456789012:my-topic with the ID of your function, topic, and full ARN.

To subscribe your function to an SNS topic, complete the following steps:

  1. Open the Amazon SNS console.
  2. In the navigation pane, choose Topics.
  3. Identify the SNS topic that's used in Amazon SES for bounce notifications. For example: An SNS topic named ses_notifications_repo.
  4. Choose the SNS topic's ARN.
  5. Choose Create Subscription.
  6. For Protocol, choose AWS Lambda.
  7. For Endpoint, enter your Lambda function's ARN.
  8. Choose Create Subscription.
  9. (Optional) Repeat the previous steps for each notification topic that you want to subscribe to your function.

Lambda console

To use the Lambda console to subscribe your Lambda function to one or more SNS topics, complete the following steps:

  1. Open the Lambda console.
  2. In the navigation page, choose Functions.
  3. Choose your Lambda function.
  4. On the Function overview pane, choose +Add trigger.
  5. In the Trigger configuration dropdown list, choose SNS.
  6. In the SNS topic dropdown list, select the SNS topic.
  7. Choose Add.
  8. (Optional) Repeat the previous steps each notification topic that you want to subscribe to your function.

Test the set up

To send a test Amazon SES message, use one of the available mailbox simulator addresses.

Note: When you use one of the mailbox simulator addresses to send test messages, the simulator prevents a negative effect on your SES deliverability metrics.

When you send the test message, Amazon SES publishes a notification to the SNS topic. Then, Amazon SNS delivers the notification to Lambda as a JSON-escaped SES event notification object in the SNS event object.

To use the Lambda console to create sample events for local testing, see Examples of event data that Amazon SES publishes to Amazon SNS.

Important: To send test messages in the Lambda console, you can't use the examples of event data as they're written. You must change the eventType message key to notificationType. If you don't change the message key, then the test fails.

Download a report from DynamoDB to view Amazon SES notifications

To query, sort, and download the contents of the DynamoDB table as a .csv file, complete the following steps:

  1. Open the DynamoDB console.
  2. Choose the SESNotifications table.
  3. Choose the Items tab.
  4. Create a Query or Scan search. For more information, see Best practices for querying and scanning data.
    Note: You can use DynamoDB table export to schedule a download of the file to an Amazon Simple Storage Service (Amazon S3) bucket at regular intervals. For more information, see DynamoDB data export to Amazon S3: how it works.

Related information

Fanout to Lambda functions

Invoking AWS Lambda functions via Amazon SNS

Receiving Amazon SES notifications using Amazon SNS

AWS OFFICIAL
AWS OFFICIALUpdated 4 days ago
4 Comments

Good day. What language and version should be used for the lambda? I'm getting some errors with:

var SnsPublishTime = event.Records[0].Sns.Timestamp;

var SnsTopicArn = event.Records[0].Sns.TopicArn;

var SESMessage = event.Records[0].Sns.Message;

Also, is there something else that needs to be done to the lambda for it to work with the SES notifications?

Thank you!

Edit: After checking the attached video, it seems this code works with Node.js 12, but that's no longer available in lambda.

Somehow I made it work with Node.js 14 (still available on lambda). The following lines have to be modified :

var SnsPublishTime = event.Records[0].Sns.Timestamp;

var SnsTopicArn = event.Records[0].Sns.TopicArn;

var SESMessage = event.Records[0].Sns.Message;

Also, the IAM role has to have the full DynamoDB access policy.

replied 4 months ago

Thank you for your comment. We'll review and update the Knowledge Center article as needed.

profile pictureAWS
MODERATOR
replied 4 months ago

Can you please update the code to work with newer versions of node.js. Trusted advisor recommended upgrading to node.js 20, however it now fails with error: Runtime.ImportModuleError: Error: Cannot find module 'aws-sdk'

I'm not a programmer, just used this code for many years - was previously able to upgrade from node.js 12 to 14 to 16

replied 2 months ago

Thank you for your comment. We'll review and update the Knowledge Center article as needed.

profile pictureAWS
MODERATOR
replied 2 months ago