Monitoring AWS SES Templated mail failures


Problem Statement

You are sending transactional emails with the aid of the SES Template Email functionality. You run some tests, but there is a problem, you are not receiving any mails: you check your logs, no errors.

I found myself in this scenario some time back. I had to go back to the developer guide, and read the fine print. At the very top of the page, it recommends you set up notifications for invalid personalization content. This post will walk you through setting up such notifications via email and lambda.

Setting up

I spent quite some time trying to setup this demo entirely with the serverless framework and I was quite surprised there was no support for managing some SES settings from serverless. That said, i got some great help setting up from this article. The final code for this walkthrough can be found on github.

Wiring up the Notifications

The notifications need a couple of things:

  1. First, a Configuration set, these add extra functionality to your emails, and in this case we will be concerned with RenderingFailures.
  2. An SNS Topic to route messages from the configuration set above.
  3. The SNS Topic can have lots of subscriptions (SQS, Email, Lambda, etc), but in this case we will use the Lambda and Email protocols.
  4. Updating your email sending code to use the Configuration set from 1 above

Creating the SNS Topic

We will create an SNS Topic which will be the destination for the notifications. We set up with help from the serverless guides

resources:
  Resources:
    SESRenderingTemplateFailureTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: SESRenderingFailureTopic

Creating the SNS Topic Subscriptions

We will create two subscriptions for the SNS Topic. I find it easier during development to have the notifications come through via email. You will need to verify this subscription as AWS will send a confirmation email, which you must acknowleged by clicking on the confirmation link to confirm your subscription.

You can hardcode your email address when you run the template, replacing Endpoint: foo@example.com with your email address.

resources:
  Resources:
    SESRenderingTemplateFailureEmailSubscription:
      Type: AWS::SNS::Subscription
      Properties:
        Endpoint: foo@example.com
        Protocol: "email"
        TopicArn: !Ref SESRenderingTemplateFailureTopic

I have also created a lambda subscription which is valuable in more formal arrangement as you can then have a more central way of logging these errors.

functions:
  monitorFailuresFn:
    handler: ...
    events:
      - sns:
          arn: !Ref SESRenderingTemplateFailureTopic
          topicName: SESRenderingFailureTopic

Creating the Configuration Set

As of writing this article, I couldnt find a way to wire this up with the serverless framework so we will do this on the AWS console. Create a configuration set specifying a suitable name

AWS Console view of creating a configuration set

Next, switch to the Event Destinations tab and Add a new destination.

AWS Console view of adding an event destination to the Config set

In Select Event Types tick, Rendering Failures

AWS Console view of selecting event types

Next, in Specify Destination select Amazon SNS enter a suitable destination name, then select your SNS Topic (created earlier)

AWS Console view of specifying destination

Specify Configuration set

One thing is missing, we have to instruct ses to use our configuration set when sending the templated email. We do that by adding the configuration set name to the sendTemplateEmail parameters like so

const sendTemplateEmailParams = {
    Template: templateName,
    Destination: {
      ToAddresses: [sendTo],
    },
    Source: process.env.SOURCE_EMAIL_ADDRESS, // use the SES domain or email verified in your account
    TemplateData: JSON.stringify(data || {}),
    ConfigurationSetName: '<your config set name>'
  };

  const resp = await sesClient
    .sendTemplatedEmail(sendTemplateEmailParams)
    .promise();

Testing it all out

A small env file has been added, which is quite helpful to potentially inject sensitive values. To test this all out, ive added a few more lambda functions. You can start the app locally via the serverless offline plugin.

./node_modules/serverless/bin/serverless.js offline start

You should get an email from AWS like so

{
   "eventType":"Rendering Failure",
   "mail":{
      "timestamp":"2024-04-06T13:04:00.928Z",
      "source":"your_email@example.com",
      "sourceArn":"arn:aws:ses:us-east-1:*********:identity/your_email@example.com",
      "sendingAccountId":"*********",
      "messageId":"0100018eb3823d0d-18c8f590-c453-4a7a-b668-0bdd51df78d3-000000",
      "destination":[
         "your_email@example.com"
      ],
      "headersTruncated":false,
      "tags":{
         "ses:source-tls-version":[
            "TLSv1.3"
         ],
         "ses:operation":[
            "SendTemplatedEmail"
         ],
         "ses:configuration-set":[
            "EmailFailures"
         ],
         "ses:source-ip":[
            "89.154.69.34"
         ],
         "ses:from-domain":[
            "example.com"
         ],
         "ses:caller-identity":[
            "root"
         ]
      }
   },
   "failure":{
      "errorMessage":"Attribute 'name' is not present in the rendering data.",
      "templateName":"DummyTemplate"
   }
}

And your lambda should also get an event with similar payload.

The full serverless template is given below:

service: sesmonitoring
frameworkVersion: '3'

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs16.x
  region: 'us-east-1'
  stage: ${opt:stage, 'dev'}
  environment:
    AWS_SDK_LOAD_CONFIG: '1'
    AWS_SES_REGION: ${aws:region}
    DEFAULT_TO_ADDRESS: ${file(env.yml):DEFAULT_TO_ADDRESS}
    SOURCE_EMAIL_ADDRESS: ${file(env.yml):SOURCE_EMAIL_ADDRESS}
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ses:SendTemplateEmail"
        - "ses:CreateTemplate"
        - "ses:ListTemplates"
      Resource: "*"


resources:
  Resources:
    SESRenderingTemplateFailureTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: SESRenderingFailureTopic
    SESRenderingTemplateFailureEmailSubscription:
      Type: AWS::SNS::Subscription
      Properties:
        Endpoint: foo@example.com
        Protocol: "email"
        TopicArn: !Ref SESRenderingTemplateFailureTopic

functions:
  monitorFailuresFn:
    handler: functions/main.logSESFailureEvent
    events:
      - sns:
          arn: !Ref SESRenderingTemplateFailureTopic
          topicName: SESRenderingFailureTopic
  createTemplate:
    handler: functions/main.createTemplate
    events:
      - httpApi:
          path: /mailing/templates
          method: post
    timeout: 15
  getTemplates:
    handler: functions/main.listTemplates
    events:
      - httpApi:
          path: /mailing/templates
          method: get
    timeout: 5
  consume:
    handler: functions/main.sendEmail
    environment: ${file(env.yml)}
    events:
      - httpApi:
          path: /mailing/send
          method: post
    timeout: 10
    




Originally published on April 5, 2024.
aws ses serverless