Managing bounced emails with AWS SES and Azure Functions

Handling bounced emails is something that is often forgotten. You may think that it isn't a problem but when sending out a lot of emails, implementing a good bounce strategy is key. In this post, I will show you how to implement a local list of bounced emails with AWS Simple Email Service and Azure Functions.

Let's start by putting a few more words on bounced emails and why implementing a good bounce strategy is important. As you already know, sending emails to email addresses that don't exist (or was deleted since you got your hands on it), causes a bounce. Most email providers won't allow you to keep sending emails to bounced email addresses. In the worst case, your email reputation will go down and the provider may even shut down your account.

Sending emails by hand? No problem, since you will get a bounce reply in your inbox. But when sending out thousands or hundreds of thousands of transactional emails, you need to implement an automated way of handling bounces.

How you want to implement this heavily depends on the email provider you have chosen. Some providers automatically detect when you try to send an email to a bounced address and won't shut you down when trying so. Others require you to handle bounces, like Simple Email Service (SES) from AWS that I will use in this post. Even though you use a provider with built-in bounce support, they probably charge you for sending emails to bounced email addresses anyway (you don't want that).

With AWS SES you will need to implement a public API that AWS can call every time an email bounced. In this post, I have chosen Azure Functions as the web platform, but that could be everything able to server requests over HTTPS (like ASP.NET Core).

We only need to implement a single endpoint so start by creating a new Azure Functions project:

Pick Http trigger as the trigger for the new function:

A new static class named Function1 is created with a static method named Run. I always start with clearing most of the content from the method and removing static from both the class and the method. This will serve as a good starting point for our bounce endpoint:

public class Bounced
{
    [FunctionName("Bounced")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req)
    {
        return new OkResult();
    }
}

By making the class and method not static, we can use dependency injection known from ASP.NET Core. Install the Microsoft.Azure.Functions.Extensions NuGet package:

Install-Package Microsoft.Azure.Functions.Extensions

Then add a new file named Startup.cs to the root of the project and include the following content:

using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(MyBounceService.Startup))]

namespace MyBounceService
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
        }
    }
}

We will add some code later and the Configure method is where you want to include all of your initialization code in a real-life scenario.

Monitor Azure websites, functions, and more with elmah.io

➡️ elmah.io for Azure ⬅️

When launching the project we now have a single HTTP POST endpoint as shown in the console window:

The api/Bounced endpoint will eventually be called by AWS SES. While only a single endpoint, the request needs to handle two different request types. Besides the obvious Email bounced type, we also need to implement a subscription confirmation. When setting up the new endpoint on AWS later in this post, SES will send a special type of request to verify the endpoint.

Let's implement a basic structure handling these two types of requests in the Bounced function:

string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);

switch (data.Type)
{
    case "Notification":
        await HandleNotificationAsync(data);
        break;
    case "SubscriptionConfirmation":
        await HandleSubscriptionConfirmationAsync(data);
        break;
}

return new OkResult();

This code simply reads the posted body and deserialize it to a dynamic using Newtonsoft.Json. I'm fully aware of different ways to do this and alternatives to Newtonsoft.Json but this code will serve fine for this example. Once deserialized, we can switch on the Type property and call either HandleNotificationAsync (in case of a bounced email) or the HandleSubscriptionConfirmationAsync method (in case of the confirmation step I mentioned earlier).

Implementing the HandleSubscriptionConfirmationAsync method is easy. We will need a HttpClient to make a callback to AWS, so start by including one in the Startup.cs file:

builder.Services.AddHttpClient("confirmationClient");

Then add a constructor to the function class:

private readonly IHttpClientFactory httpClientFactory;

public Bounced(IHttpClientFactory httpClientFactory)
{
    this.httpClientFactory = httpClientFactory;
}

Implementing the HandleSubscriptionConfirmationAsync method can be done like this:

private async Task HandleSubscriptionConfirmationAsync(dynamic data)
{
    string subscriptionUrl = data.SubscribeURL;
    if (!string.IsNullOrWhiteSpace(subscriptionUrl) && Uri.TryCreate(subscriptionUrl, UriKind.Absolute, out Uri result))
    {
        var client = httpClientFactory.CreateClient("confirmationClient");
        await client.GetAsync(result);
    }
}

When we get a request of type SubscriptionConfirmation we make an HTTP callback on the SubscribeURL provided by AWS.

Next up is the actual bounce notification. A simple implementation of the HandleNotificationAsync could look like this:

private async Task HandleNotificationAsync(dynamic data)
{
    string messageJSON = data.Message;
    dynamic message = JsonConvert.DeserializeObject(messageJSON);
    dynamic bounce = message.bounce;
    JArray bouncedRecipients = bounce.bouncedRecipients;

    foreach (dynamic bouncedRecipient in bouncedRecipients)
    {
        string emailAddress = bouncedRecipient.emailAddress;
        // TODO: store the email address somewhere
    }
}

Quick and dirty without any null checks or anything like that (you may want some of that). The code pulls a list of bounced emails from the Message.bounce.bouncedRecipients array. I've left a TODO in the code for you to implement. Where you want to store bounced email addresses is up to your system. It could be as files on the file system, as rows in a database, or something else. As long as it is somewhere your email code can reach and check if an email should be sent or not.

You can test the two scenarios locally by using the following PowerShell scripts:

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "{
`n  `"Type`" : `"SubscriptionConfirmation`",
`n  `"SubscribeURL`" : `"https://google.com`"
`n}"

$response = Invoke-RestMethod 'http://localhost:7071/api/Bounced' -Method 'POST' -Headers $headers -Body $body
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/json")

$body = "{
`n  `"Type`" : `"Notification`",
`n  `"Message`": `"{`\`"notificationType`\`": `\`"Bounce`\`", `\`"bounce`\`": {`\`"bounceType`\`": `\`"Permanent`\`", `\`"bouncedRecipients`\`": [{`\`"emailAddress`\`": `\`"bouncing@mydomain.com`\`"}]}}`"
`n}"

$response = Invoke-RestMethod 'http://localhost:7071/api/Bounced' -Method 'POST' -Headers $headers -Body $body

Deploy the function app to Azure using your favorite CI/CD pipeline (for an example of using Azure DevOps, check out Continuous deployment to Azure from Azure DevOps). Then go to the AWS Console. Go to Simple Notification Service and create a new topic with a name of your choice. Then create a new subscription and add the topic from the previous step in Topic ARN and select HTTPS in Protocol. The URL to input is the absolute URL of the Bounced function that you deployed to Azure in the previous step. It will look similar to this:

https://your-function-app.azurewebsites.net/api/Bounced?code=abc123

Where your-function-app is the subdomain name of your function app and code contains the function key needed to call this function. Function keys can be found through the Azure Portal by clicking the function and selecting Function Keys.

The final step missing is telling AWS SES to put a message on the AWS topic when an email bounces. This can be done by going to SES, Clicking Domains beneath Identify Management, clicking your verified domain (verify your domain if you haven't already done that), and expanding Notifications. Here you can select the topic already created in the Bounces dropdown:

As part of this step, AWS will call your Azure Function and ask for confirmation (remember the HandleSubscriptionConfirmationAsync?). In theory, you can remove that code from the function once called once. I wouldn't do that since the need to re-authenticate this at a later point in time will force you to re-introduce the code.

That's it. AWS will now put a notification on a topic every time an email bounces. The subscription on that topic will call the Azure Function with the bounced email. If you need help sending emails in .NET, check out How to send emails from C#/.NET - The definitive tutorial.