AWS Lambda + .NETでメールステータスWebhookを受け取る

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

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

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

本記事では実際にどういった内容が送られてくるのかを紹介します。

受け取るWebhookの設定

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

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

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

メール送信した直後

メール送信を行うと、そのデータがキューに入ります。そして、以下のようなWebhookが送られてきます(データは一部マスキングしています)。

{
    "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"
            }
        ]
    }
}

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 に変更します。

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";
    }
}

送られてくるデータに合わせて、JSONデータをマッピングする MailWebhookData クラスを定義します。

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から受信するメールイベントのルートオブジェクト
/// event_typeによって配信情報またはバウンス情報を含みます
/// </summary>
public class MailWebhookData
{
    /// <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>
    /// イベントデータ(配信情報またはバウンス情報)
    /// </summary>
    [JsonPropertyName("event")]
    public EventData Event { get; set; } = new();
}

/// <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>
    /// 配信の理由やステータス詳細
    /// </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"など)
    /// </summary>
    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;
}

送られてくるデータは JsonSerializer を使ってデシリアライズします。

// JSONをC#オブジェクトにデシリアライズ
var webhookData = JsonSerializer.Deserialize<MailWebhookData>(request.Body);
if (webhookData == null)
{
    context.Logger.LogError("Failed to deserialize webhook data");
    return JsonSerializer.Serialize(new { error = "Invalid JSON data" });
}

後は、eventType の値によって、処理を分けます。

// フィールドデータの出力
context.Logger.LogInformation($"Server Composition: {webhookData.ServerComposition}");
// EventDataを取得
if (webhookData.Event.Deliveries != null)
{
    context.Logger.LogInformation($"Deliveries count: {webhookData.Event.Deliveries.Count}");
    context.Logger.LogInformation($"Delivery Subject: {webhookData.Event.Deliveries.FirstOrDefault()?.Subject}");
}
if (webhookData.Event.Bounces != null)
{
    context.Logger.LogInformation($"Bounces count: {webhookData.Event.Bounces.Count}");
    context.Logger.LogInformation($"Bounce Subject: {webhookData.Event.Bounces.FirstOrDefault()?.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から受信するメールイベントのルートオブジェクト
/// event_typeによって配信情報またはバウンス情報を含みます
/// </summary>
public class MailWebhookData
{
    /// <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>
    /// イベントデータ(配信情報またはバウンス情報)
    /// </summary>
    [JsonPropertyName("event")]
    public EventData Event { get; set; } = new();
}

/// <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>
    /// 配信の理由やステータス詳細
    /// </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"など)
    /// </summary>
    [JsonPropertyName("status")]
    public string Status { get; set; } = string.Empty;
}

/// <summary>
/// AWS Lambda関数のメインクラス
/// メールWebhookのJSONデータを処理します
/// </summary>
public class Function
{
    /// <summary>
    /// Lambda関数のエントリーポイント
    /// API GatewayからのJSONリクエストを受け取り、メールイベントを処理します
    /// </summary>
    /// <param name="request">API Gatewayからのリクエスト情報</param>
    /// <param name="context">Lambda実行時の情報(ログ出力などに使用)</param>
    /// <returns>処理結果のJSON文字列</returns>
    public static string FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
    {
        try
        {
            // JSONをC#オブジェクトにデシリアライズ
            var webhookData = JsonSerializer.Deserialize<MailWebhookData>(request.Body);

            if (webhookData == null)
            {
                context.Logger.LogError("Failed to deserialize webhook data");
                return JsonSerializer.Serialize(new { error = "Invalid JSON data" });
            }
            // フィールドデータの出力
            context.Logger.LogInformation($"Server Composition: {webhookData.ServerComposition}");
            // EventDataを取得
            if (webhookData.Event.Deliveries != null)
            {
                context.Logger.LogInformation($"Deliveries count: {webhookData.Event.Deliveries.Count}");
                context.Logger.LogInformation($"Delivery Subject: {webhookData.Event.Deliveries.FirstOrDefault()?.Subject}");
            }
            if (webhookData.Event.Bounces != null)
            {
                context.Logger.LogInformation($"Bounces count: {webhookData.Event.Bounces.Count}");
                context.Logger.LogInformation($"Bounce Subject: {webhookData.Event.Bounces.FirstOrDefault()?.Subject}");
            }
            return "ok";
        }
        catch (JsonException ex)
        {
            context.Logger.LogError($"JSON parsing error: {ex.Message}");
            return JsonSerializer.Serialize(new { error = "Invalid JSON format", details = ex.Message });
        }
        catch (Exception ex)
        {
            context.Logger.LogError($"Unexpected error: {ex.Message}");
            return JsonSerializer.Serialize(new { error = "Internal server error", details = ex.Message });
        }
    }
}

たとえば、以下のような出力が得られます。

2025-06-06T06:50:03.004Z  16a05f26-5ce5-4549-a486-0aef24edbfb0    info    Server Composition:  pro
2025-06-06T06:50:03.004Z    16a05f26-5ce5-4549-a486-0aef24edbfb0    info    Deliveries count: 2
2025-06-06T06:50:03.005Z    16a05f26-5ce5-4549-a486-0aef24edbfb0    info    Delivery Subject: メールマガジンのテスト

まとめ

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

このWebhookはSMTP経由の場合、利用できます。ぜひご利用ください。