AWS Lambda + .NETでapplication/x-www-form-urlencodedのメール送信ステータスWebhookを処理する

Customers Mail CloudのWebhookは2種類あります。

  1. メール受信時
  2. メール送信時

メール送信時は、送信したメールに対してステータスが変わったタイミングで通知が送られるものです。

その際、 application/json を指定しない設定ができます。この時のデータがどうなっているのか紹介します。

<!—more—>

AWS Lambdaの準備

AWS Lambdaで新しい関数を作成します。その際、条件として以下を指定します。

  • ランタイム
    .NET 8 (C#/F#/PowerShell)
  • 関数 URL を有効化
    チェックを入れる
  • 認証タイプ
    NONE

関数の作成が完了したら、関数コードを作成します。これは、ローカルで実行します。

dotnet tool install -g Amazon.Lambda.Tools 
dotnet new lambda.EmptyFunction --name ExampleCS

このコマンドを実行すると、以下のような構成でプロジェクトが作成されます。

.
├── src
│   └── ExampleCS
│       ├── aws-lambda-tools-defaults.json
│       ├── ExampleCS.csproj
│       ├── Function.cs
│       └── Readme.md
└── test
    └── ExampleCS.Tests
        ├── ExampleCS.Tests.csproj
        └── FunctionTest.cs

4 directories, 6 files

この中の、 Function.cs に処理を記述します。ベースになるコードは以下の通りです。実際に処理を行うのは FunctionHandler メソッドです。

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ExampleCS;

public class Function
{
    
    /// <summary>
    /// A simple function that takes a string and does a ToUpper
    /// </summary>
    /// <param name="input">The event for the Lambda function handler to process.</param>
    /// <param name="context">The ILambdaContext that provides methods for logging and describing the Lambda environment.</param>
    /// <returns></returns>
    public string FunctionHandler(string input, ILambdaContext context)
    {
        return input.ToUpper();
    }
}

また、Lambdaの管理画面にて、ハンドラーの設定を ExampleCS::ExampleCS.Function::FunctionHandler に変更します。

受け取るWebhookの設定

管理画面にて、受け取るWebhookを設定できます。設定は以下が用意されています。

  • Bounces
    • bounced(エラーメールを受け取る)
  • Deliveries
    • queued(キューに入ったタイミング)
    • succeeded(送信完了)
    • failed(送信失敗)
    • deferred(送信延期)

この中で application/json を指定できます。指定しなかった場合、データは application/x-www-form-urlencoded にて送信されます。本記事ではこの場合を想定しています。

送信されてくるデータについて

メール送信した直後

メール送信を行うと、そのデータがキューに入ります。そして、以下のようなWebhookが送られてきます(データは一部マスキングしています)。データは分かりやすいようにJSONにしていますが、実際には異なりますので注意してください。

{
    "event_type": "deliveries",
    "server_composition": "pro",
    "event": '{"deliveries":[{"reason":"","sourceIp":"100.100.100.1","returnPath":"info@return.pro.smtps.jp","created":"2023-01-25 14:03:06","subject":"メールマガジンのテスト","apiData":"","messageId":"<031a32d4-06cd-b1ae-9526-011c0b9f1296@example.com>","from":"info@example.com","to":"user@example.jp","senderIp":"","status":"queued"}]}'
}

メール送信完了時

Customers Mail Cloudからメール送信処理が行われると、ステータスが succeeded になったWebhookが送られてきます。

{
    "event_type": "deliveries",
    "server_composition": "pro",
    "event": '{"deliveries":[{"reason":"","sourceIp":"","returnPath":"info@return.pro.smtps.jp","created":"2023-01-25 14:03:09","subject":"メールマガジンのテスト","apiData":"","messageId":"<031a32d4-06cd-b1ae-9526-011c0b9f1296@example.com>","from":"info@example.com","to":"user@example.jp","senderIp":"100.100.100.3","status":"succeeded"}]}'
}

メール送信失敗時(メールアドレス形式に問題がある場合)

メールアドレスの形式に問題があるなど、送信処理が失敗した場合には以下のようなWebhookが送られてきます。

{
    "event_type": "bounces",
    "server_composition": "pro",
    "event": '{"bounces":[{"reason":"host unknown","returnPath":"info@return.pro.smtps.jp","created":"2023-01-25 14:05:15","subject":"メールマガジンのテスト","apiData":"","messageId":"<8f902ee7-ae65-8711-48a8-2f708cb14205@example.com>","from":"info@example.com","to":"user@example","status":"1"}]}'
}

メール送信失敗時(送信先サーバーからエラーが返ってくる場合)

ユーザーが存在しない、メールボックスがいっぱいなど送信先サーバーからエラーが返ってきた場合には、以下のようなJSONが返ってきます。

{
    "event_type": "deliveries",
    "server_composition": "pro",
    "event": '{"deliveries":[{"reason":"550 5.1.1 The email account that you tried to reach does not exist. Please try 5.1.1 double-checking the recipient's email address for typos or 5.1.1 unnecessary spaces. Learn more at 5.1.1  <https://support.google.com/mail/?p=NoSuchUser> b197-20020a621bce000000b0058b80756b07si311029pfb.3 - gsmtp (in reply to RCPT TO)","sourceIp":"","returnPath":"info@return.pro.smtps.jp","created":"2023-01-25 14:06:06","subject":"メールマガジンのテスト","apiData":"","messageId":"<9e7e564c-ac83-8cd8-2cb4-b9ff2a9f168d@example.com>","from":"info@example.com","to":"no-user@example.jp","senderIp":"100.100.100.3","status":"failed"}]}'
}

エラーとしてのWebhookも送られてきます。上記のものと event_type が異なるので注意してください。

{
    "event_type": "bounces",
    "server_composition": "pro",
    "event": '{"bounces":[{"reason":"550 5.1.1 The email account that you tried to reach does not exist. Please try 5.1.1 double-checking the recipient's email address for typos or 5.1.1 unnecessary spaces. Learn more at 5.1.1  <https://support.google.com/mail/?p=NoSuchUser> b197-20020a621bce000000b0058b80756b07si311029pfb.3 - gsmtp (in reply to RCPT TO)","returnPath":"info@return.pro.smtps.jp","created":"2023-01-25 14:06:07","subject":"メールマガジンのテスト","apiData":"","messageId":"<9e7e564c-ac83-8cd8-2cb4-b9ff2a9f168d@example.com>","from":"info@example.com","to":"no-user@example.jp","status":"2"}]}'
}

C#のコード

処理は ExampleCS/src/ExampleCS/Function.cs に記述します。

using Amazon.Lambda.Core;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ExampleCS;

public class Function
{
    
    /// <summary>
    /// A simple function that takes a string and does a ToUpper
    /// </summary>
    /// <param name="input">The event for the Lambda function handler to process.</param>
    /// <param name="context">The ILambdaContext that provides methods for logging and describing the Lambda environment.</param>
    /// <returns></returns>
    public string FunctionHandler(string input, ILambdaContext context)
    {
        return "ok";
    }
}

また、データの入力を格納するクラス MailWebhookFormData を用意します。クラスのプロパティはCustomers Mail Cloudのドキュメントを参考にしています。

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using System.Text.Json;
using System.Text.Json.Serialization;

// Lambda関数のJSON入力を.NETクラスに変換するためのアセンブリ属性
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ExampleCS;

/// <summary>
/// Webhookから受信するメールイベントのルートオブジェクト(Form-URLEncoded形式)
/// event_typeとserver_compositionは文字列、eventフィールドはJSON文字列として受信されます
/// </summary>
public class MailWebhookFormData
{
    /// <summary>
    /// イベントタイプ("deliveries" または "bounces")
    /// </summary>
    [JsonPropertyName("event_type")]
    public string EventType { get; set; } = string.Empty;

    /// <summary>
    /// サーバー構成(例:"pro")
    /// </summary>
    [JsonPropertyName("server_composition")]
    public string ServerComposition { get; set; } = string.Empty;

    /// <summary>
    /// イベントデータのJSON文字列
    /// この文字列を更にパースしてEventDataオブジェクトに変換する必要があります
    /// </summary>
    [JsonPropertyName("event")]
    public string EventJson { get; set; } = string.Empty;
}

/// <summary>
/// イベントデータのコンテナ
/// deliveriesまたはbouncesのいずれかを含みます
/// </summary>
public class EventData
{
    /// <summary>
    /// 配信イベントのリスト(event_type が "deliveries" の場合)
    /// </summary>
    [JsonPropertyName("deliveries")]
    public List<MessageEvent>? Deliveries { get; set; }

    /// <summary>
    /// バウンスイベントのリスト(event_type が "bounces" の場合)
    /// </summary>
    [JsonPropertyName("bounces")]
    public List<MessageEvent>? Bounces { get; set; }
}

/// <summary>
/// メールイベントの詳細情報
/// 配信イベントとバウンスイベントの両方に対応した統合クラス
/// </summary>
public class MessageEvent
{
    /// <summary>
    /// イベントの理由やステータス詳細
    /// 配信の場合:配信理由、バウンスの場合:バウンス理由(例:"host unknown")
    /// </summary>
    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    /// <summary>
    /// 送信元IPアドレス(配信イベントの場合のみ)
    /// </summary>
    [JsonPropertyName("sourceIp")]
    public string SourceIp { get; set; } = string.Empty;

    /// <summary>
    /// リターンパス(返信先メールアドレス)
    /// </summary>
    [JsonPropertyName("returnPath")]
    public string ReturnPath { get; set; } = string.Empty;

    /// <summary>
    /// イベント作成日時
    /// </summary>
    [JsonPropertyName("created")]
    public string Created { get; set; } = string.Empty;

    /// <summary>
    /// メールの件名
    /// </summary>
    [JsonPropertyName("subject")]
    public string Subject { get; set; } = string.Empty;

    /// <summary>
    /// API関連データ
    /// </summary>
    [JsonPropertyName("apiData")]
    public string ApiData { get; set; } = string.Empty;

    /// <summary>
    /// メッセージID(一意識別子)
    /// </summary>
    [JsonPropertyName("messageId")]
    public string MessageId { get; set; } = string.Empty;

    /// <summary>
    /// 送信者メールアドレス
    /// </summary>
    [JsonPropertyName("from")]
    public string From { get; set; } = string.Empty;

    /// <summary>
    /// 受信者メールアドレス
    /// </summary>
    [JsonPropertyName("to")]
    public string To { get; set; } = string.Empty;

    /// <summary>
    /// 送信者IPアドレス(配信イベントの場合のみ)
    /// </summary>
    [JsonPropertyName("senderIp")]
    public string SenderIp { get; set; } = string.Empty;

    /// <summary>
    /// ステータス
    /// 配信の場合:配信ステータス(例:"queued", "sent")
    /// バウンスの場合:バウンスステータスコード(例:"1")
    /// </summary>
    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;
}

全体の流れ

処理の流れは以下の通りです。まず、 APIGatewayProxyRequest を受け取り、リクエストボディをパースします。その後、 ParseFormUrlEncodedData メソッドにて、 MailWebhookFormDataEventData オブジェクトに変換します。

MailWebhookFormData webhookFormData;
EventData eventData;

(webhookFormData, eventData) = ParseFormUrlEncodedData(request.Body, request.IsBase64Encoded, context);

ParseFormUrlEncodedData メソッドは、リクエストボディをデコードし、URLエンコードされたフォームデータをパースして MailWebhookFormDataEventData オブジェクトに変換します。

event フィールドはJSON文字列として受信されるため、さらに EventData オブジェクトにデシリアライズします。

リクエストボディのパース

/// <summary>
/// Form-URLEncodedデータを解析してC#オブジェクトに変換します
/// eventフィールドのJSON文字列も同時にパースします
/// </summary>
/// <param name="body">リクエストボディ</param>
/// <param name="isBase64Encoded">ボディがBase64エンコードされているかどうか</param>
/// <param name="context">Lambda実行コンテキスト</param>
/// <returns>解析されたWebhookデータとイベントデータ</returns>
private static (MailWebhookFormData webhookData, EventData eventData) ParseFormUrlEncodedData(string body, bool isBase64Encoded, ILambdaContext context)
{
    try
    {
        // Form-URLEncodedデータをパース
        var formData = new MailWebhookFormData();
        var eventData = new EventData();

        context.Logger.LogInformation($"IsBase64Encoded: {isBase64Encoded}");
        context.Logger.LogInformation($"Original body: {body}");

        // Base64デコードが必要な場合
        string rawBody = body;
        if (isBase64Encoded)
        {
            try
            {
                var base64Bytes = Convert.FromBase64String(body);
                rawBody = System.Text.Encoding.UTF8.GetString(base64Bytes);
                context.Logger.LogInformation($"Base64 decoded body: {rawBody}");
            }
            catch (Exception ex)
            {
                context.Logger.LogError($"Failed to decode Base64: {ex.Message}");
                // Base64デコードに失敗した場合でも、元の文字列で試行を続ける
            }
        }

        // URLデコードして key=value ペアに分割
        var urlDecodedBody = Uri.UnescapeDataString(rawBody);
        context.Logger.LogInformation($"URL decoded body: {urlDecodedBody}");

        var keyValuePairs = urlDecodedBody.Split('&');
        
        foreach (var pair in keyValuePairs)
        {
            var keyValue = pair.Split('=', 2); // 最大2つに分割(値に=が含まれる可能性があるため)
            if (keyValue.Length == 2)
            {
                var key = Uri.UnescapeDataString(keyValue[0]);
                var value = Uri.UnescapeDataString(keyValue[1]);

                switch (key)
                {
                    case "event_type":
                        formData.EventType = value;
                        break;
                    case "server_composition":
                        formData.ServerComposition = value;
                        break;
                    case "event":
                        formData.EventJson = value;
                        // eventフィールドのJSON文字列をパース
                        context.Logger.LogInformation($"Parsing event JSON: {value}");
                        eventData = JsonSerializer.Deserialize<EventData>(value) ?? new EventData();
                        break;
                    default:
                        context.Logger.LogInformation($"Unknown field: {key} = {value}");
                        break;
                }
            }
        }

        return (formData, eventData);
    }
    catch (JsonException ex)
    {
        context.Logger.LogError($"JSON parsing error in event field: {ex.Message}");
        throw new Exception($"Failed to parse event JSON: {ex.Message}");
    }
}

ログ出力

今回は、最後にデバッグ用にログ出力を行います。 context.Logger を使用して、処理したイベントの情報を出力します。

context.Logger.LogInformation($"Processed event type: {webhookFormData.EventType}");
context.Logger.LogInformation($"Processed server composition: {webhookFormData.ServerComposition}");
if (eventData.Deliveries != null)
{
    context.Logger.LogInformation($"Number of deliveries: {eventData.Deliveries.Count}");
    context.Logger.LogInformation($"First delivery subject: {eventData.Deliveries.FirstOrDefault()?.Subject ?? "N/A"}");
}
if (eventData.Bounces != null)
{
    context.Logger.LogInformation($"Number of bounces: {eventData.Bounces.Count}");
    context.Logger.LogInformation($"First bounce subject: {eventData.Bounces.FirstOrDefault()?.Subject ?? "N/A"}");
}

出力例は以下の通りです。

2025-06-06T07:10:05.048Z  9c7c503d-633c-49b3-8f5f-d86e6371a6ca    info    Processed event type: bounces
2025-06-06T07:10:05.048Z    9c7c503d-633c-49b3-8f5f-d86e6371a6ca    info    Number of bounces: 1
2025-06-06T07:10:05.048Z    9c7c503d-633c-49b3-8f5f-d86e6371a6ca    info    First bounce subject: メールマガジンのテスト

全体のコード

全体のコードは以下の通りです。

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using System.Text.Json;
using System.Text.Json.Serialization;

// Lambda関数のJSON入力を.NETクラスに変換するためのアセンブリ属性
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ExampleCS;

/// <summary>
/// Webhookから受信するメールイベントのルートオブジェクト(Form-URLEncoded形式)
/// event_typeとserver_compositionは文字列、eventフィールドはJSON文字列として受信されます
/// </summary>
public class MailWebhookFormData
{
    /// <summary>
    /// イベントタイプ("deliveries" または "bounces")
    /// </summary>
    [JsonPropertyName("event_type")]
    public string EventType { get; set; } = string.Empty;

    /// <summary>
    /// サーバー構成(例:"pro")
    /// </summary>
    [JsonPropertyName("server_composition")]
    public string ServerComposition { get; set; } = string.Empty;

    /// <summary>
    /// イベントデータのJSON文字列
    /// この文字列を更にパースしてEventDataオブジェクトに変換する必要があります
    /// </summary>
    [JsonPropertyName("event")]
    public string EventJson { get; set; } = string.Empty;
}

/// <summary>
/// イベントデータのコンテナ
/// deliveriesまたはbouncesのいずれかを含みます
/// </summary>
public class EventData
{
    /// <summary>
    /// 配信イベントのリスト(event_type が "deliveries" の場合)
    /// </summary>
    [JsonPropertyName("deliveries")]
    public List<MessageEvent>? Deliveries { get; set; }

    /// <summary>
    /// バウンスイベントのリスト(event_type が "bounces" の場合)
    /// </summary>
    [JsonPropertyName("bounces")]
    public List<MessageEvent>? Bounces { get; set; }
}

/// <summary>
/// メールイベントの詳細情報
/// 配信イベントとバウンスイベントの両方に対応した統合クラス
/// </summary>
public class MessageEvent
{
    /// <summary>
    /// イベントの理由やステータス詳細
    /// 配信の場合:配信理由、バウンスの場合:バウンス理由(例:"host unknown")
    /// </summary>
    [JsonPropertyName("reason")]
    public string Reason { get; set; } = string.Empty;

    /// <summary>
    /// 送信元IPアドレス(配信イベントの場合のみ)
    /// </summary>
    [JsonPropertyName("sourceIp")]
    public string SourceIp { get; set; } = string.Empty;

    /// <summary>
    /// リターンパス(返信先メールアドレス)
    /// </summary>
    [JsonPropertyName("returnPath")]
    public string ReturnPath { get; set; } = string.Empty;

    /// <summary>
    /// イベント作成日時
    /// </summary>
    [JsonPropertyName("created")]
    public string Created { get; set; } = string.Empty;

    /// <summary>
    /// メールの件名
    /// </summary>
    [JsonPropertyName("subject")]
    public string Subject { get; set; } = string.Empty;

    /// <summary>
    /// API関連データ
    /// </summary>
    [JsonPropertyName("apiData")]
    public string ApiData { get; set; } = string.Empty;

    /// <summary>
    /// メッセージID(一意識別子)
    /// </summary>
    [JsonPropertyName("messageId")]
    public string MessageId { get; set; } = string.Empty;

    /// <summary>
    /// 送信者メールアドレス
    /// </summary>
    [JsonPropertyName("from")]
    public string From { get; set; } = string.Empty;

    /// <summary>
    /// 受信者メールアドレス
    /// </summary>
    [JsonPropertyName("to")]
    public string To { get; set; } = string.Empty;

    /// <summary>
    /// 送信者IPアドレス(配信イベントの場合のみ)
    /// </summary>
    [JsonPropertyName("senderIp")]
    public string SenderIp { get; set; } = string.Empty;

    /// <summary>
    /// ステータス
    /// 配信の場合:配信ステータス(例:"queued", "sent")
    /// バウンスの場合:バウンスステータスコード(例:"1")
    /// </summary>
    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;
}

/// <summary>
/// AWS Lambda関数のメインクラス
/// メールWebhookのForm-URLEncodedデータを処理します
/// </summary>
public class Function
{
    /// <summary>
    /// Lambda関数のエントリーポイント
    /// API Gatewayからのapplication/x-www-form-urlencodedリクエストを受け取り、メールイベントを処理します
    /// </summary>
    /// <param name="request">API Gatewayからのリクエスト情報</param>
    /// <param name="context">Lambda実行時の情報(ログ出力などに使用)</param>
    /// <returns>処理結果のJSON文字列</returns>
    public static string FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
    {
        try
        {
            MailWebhookFormData webhookFormData;
            EventData eventData;

            (webhookFormData, eventData) = ParseFormUrlEncodedData(request.Body, request.IsBase64Encoded, context);

            context.Logger.LogInformation($"Processed event type: {webhookFormData.EventType}");
            context.Logger.LogInformation($"Processed server composition: {webhookFormData.ServerComposition}");
            if (eventData.Deliveries != null)
            {
                context.Logger.LogInformation($"Number of deliveries: {eventData.Deliveries.Count}");
                context.Logger.LogInformation($"First delivery subject: {eventData.Deliveries.FirstOrDefault()?.Subject ?? "N/A"}");
            }
            if (eventData.Bounces != null)
            {
                context.Logger.LogInformation($"Number of bounces: {eventData.Bounces.Count}");
                context.Logger.LogInformation($"First bounce subject: {eventData.Bounces.FirstOrDefault()?.Subject ?? "N/A"}");
            }
            return "ok";
        }
        catch (Exception ex)
        {
            context.Logger.LogError($"Error processing webhook data: {ex.Message}");
            return JsonSerializer.Serialize(new { error = "Internal server error", details = ex.Message });
        }
    }

    /// <summary>
    /// Form-URLEncodedデータを解析してC#オブジェクトに変換します
    /// eventフィールドのJSON文字列も同時にパースします
    /// </summary>
    /// <param name="body">リクエストボディ</param>
    /// <param name="isBase64Encoded">ボディがBase64エンコードされているかどうか</param>
    /// <param name="context">Lambda実行コンテキスト</param>
    /// <returns>解析されたWebhookデータとイベントデータ</returns>
    private static (MailWebhookFormData webhookData, EventData eventData) ParseFormUrlEncodedData(string body, bool isBase64Encoded, ILambdaContext context)
    {
        try
        {
            // Form-URLEncodedデータをパース
            var formData = new MailWebhookFormData();
            var eventData = new EventData();

            context.Logger.LogInformation($"IsBase64Encoded: {isBase64Encoded}");
            context.Logger.LogInformation($"Original body: {body}");

            // Base64デコードが必要な場合
            string rawBody = body;
            if (isBase64Encoded)
            {
                try
                {
                    var base64Bytes = Convert.FromBase64String(body);
                    rawBody = System.Text.Encoding.UTF8.GetString(base64Bytes);
                    context.Logger.LogInformation($"Base64 decoded body: {rawBody}");
                }
                catch (Exception ex)
                {
                    context.Logger.LogError($"Failed to decode Base64: {ex.Message}");
                    // Base64デコードに失敗した場合でも、元の文字列で試行を続ける
                }
            }

            // URLデコードして key=value ペアに分割
            var urlDecodedBody = Uri.UnescapeDataString(rawBody);
            context.Logger.LogInformation($"URL decoded body: {urlDecodedBody}");

            var keyValuePairs = urlDecodedBody.Split('&');
            
            foreach (var pair in keyValuePairs)
            {
                var keyValue = pair.Split('=', 2); // 最大2つに分割(値に=が含まれる可能性があるため)
                if (keyValue.Length == 2)
                {
                    var key = Uri.UnescapeDataString(keyValue[0]);
                    var value = Uri.UnescapeDataString(keyValue[1]);

                    switch (key)
                    {
                        case "event_type":
                            formData.EventType = value;
                            break;
                        case "server_composition":
                            formData.ServerComposition = value;
                            break;
                        case "event":
                            formData.EventJson = value;
                            // eventフィールドのJSON文字列をパース
                            context.Logger.LogInformation($"Parsing event JSON: {value}");
                            eventData = JsonSerializer.Deserialize<EventData>(value) ?? new EventData();
                            break;
                        default:
                            context.Logger.LogInformation($"Unknown field: {key} = {value}");
                            break;
                    }
                }
            }

            return (formData, eventData);
        }
        catch (JsonException ex)
        {
            context.Logger.LogError($"JSON parsing error in event field: {ex.Message}");
            throw new Exception($"Failed to parse event JSON: {ex.Message}");
        }
    }
}

まとめ

Webhookを使うことで、メール送信ステータスの変化に応じて通知を受け取れるようになります。メールと連携したシステムを開発する際に役立つでしょう。

AWS Lambdaの場合は application/json を指定した方が全体として、受け取りやすい印象です。ぜひお試しください。なお、このWebhookはSMTP経由の場合、利用できます。