Webhook APIを使ってメールを処理する(AWS Lambda + .NET)

Customers Mail Cloudではプログラム側からデータを取得したり、メールを送信するWeb APIの他に、Customers Mail Cloudでメールを受信した時にイベントを伝えてくれるWebhook APIが用意されています。

Webhook APIを使うことで、自前でメールサーバを立てずにメール受信のタイミングでシステムを起動させられるようになります。メールサーバを安定して動作させ続けるのはメンテナンスコストが大きいですが、Customers Mail Cloudを使うことで簡単にメールと連携したシステムが作れるようになるでしょう。

今回はAWS Lambdaで.NETを使ってメールを処理する流れを紹介します。

フォーマットはJSONとマルチパートフォームデータ

Webhookの形式として、JSONとマルチパートフォームデータ(multipart/form-data)が選択できます。この二つの違いは、添付ファイルがあるかどうかです。JSONの場合、添付ファイルは送られてきません。メールに添付ファイルがついてくる可能性がある場合は、後者を選択してください。

Webhook設定ダイアログ

今回はJSONフォーマットにおけるWebhook処理について紹介します。

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を使ってPOSTされるJSONデータは、次のようになっています。

{
    "filter": "info@smtps.jp",
    "headers": [
      {"name": "Return-Path", "value": "<user@example.com>"},
        :
      {"name": "Date", "value": "Thu, 27 Apr 2023 15:56:26 +0900"}
    ],
    "subject": "Webhookのテスト",
    "envelope-to": "user@smtps.jp",
    "server_composition": "sandbox",
    "html": "<div dir=\\\\\\\\\\\\\\\\"ltr\\\\\\\\\\\\\\\\">Webhookのテスト用メールです。<div>...</div></div>",
    "text": "Webhookのテスト用メールです。\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n--\\\\\\\\\\\\\\\\r\\\\\\\\\\\\\\\\n...",
    "envelope-from": "info@smtps.jp"
}

C#のコード

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

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

受け取るデータは以下のようになっており、実際に必要なのは body キー以下になります。

{
    "version": "2.0",
    "routeKey": "$default",
    "rawPath": "/",
    "rawQueryString": "",
    "headers": {
        "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256",
        "content-length": "10094",
        "x-amzn-tls-version": "TLSv1.2",
        "x-amzn-trace-id": "Root=1-682a992f-4a19331904701ec714a640cb",
        "x-forwarded-proto": "https",
        "host": "yaw...flx.lambda-url.us-west-1.on.aws",
        "x-forwarded-port": "443",
        "content-type": "application/json; charset=UTF-8",
        "x-forwarded-for": "124.110.200.16",
        "accept-encoding": "gzip,deflate",
        "user-agent": "HTTP Client"
    },
    "requestContext": {
        "accountId": "anonymous",
        "apiId": "yaw...flx",
        "domainName": "yaw...flx.lambda-url.us-west-1.on.aws",
        "domainPrefix": "yaw...flx",
        "http": {
            "method": "POST",
            "path": "/",
            "protocol": "HTTP/1.1",
            "sourceIp": "124.110.200.16",
            "userAgent": "HTTP Client"
        },
        "requestId": "11fb683f-d1b9-402d-9baa-a16e9540b4f6",
        "routeKey": "$default",
        "stage": "$default",
        "time": "19/May/2025:02:36:31 +0000",
        "timeEpoch": 1747622191634
    },
    "body": "{\"filter\":...om\"}",
    "isBase64Encoded": false
}

そこで、データを入力として受け取るクラスを用意します。

public class ApiGatewayV2HttpEvent
{
    public string Body { get; set; }
    public bool IsBase64Encoded { get; set; }
}

これに合わせて、 FunctionHandler メソッドの引数を ApiGatewayV2HttpEvent に変更します。

public string FunctionHandler(ApiGatewayV2HttpEvent request, ILambdaContext context)

JSONのパース

JSONのパースには System.Text.Json を使います。 FunctionHandler メソッドの中で、受け取ったデータをパースします。

var email = JsonSerializer.Deserialize<EmailWebhookEvent>(request.Body);

EmailWebhookEvent クラスは、受け取るデータに合わせて定義します。

using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

public class EmailWebhookEvent
{
    [JsonPropertyName("filter")]
    public string Filter { get; set; }
    [JsonPropertyName("headers")]
    public List<EmailHeader> Headers { get; set; }
    [JsonPropertyName("subject")]
    public string Subject { get; set; }
    [JsonPropertyName("envelope-to")]
    public string EnvelopeTo { get; set; }
    [JsonPropertyName("server_composition")]
    public string ServerComposition { get; set; }
    [JsonPropertyName("html")]
    public string Html { get; set; }
    [JsonPropertyName("text")]
    public string Text { get; set; }
    [JsonPropertyName("envelope-from")]
    public string EnvelopeFrom { get; set; }
}

public class EmailHeader
{
    [JsonPropertyName("name")]
    public string Name { get; set; }
    [JsonPropertyName("value")]
    public string Value { get; set; }
}

後は自由にデータを取得、出力できます。

context.Logger.LogLine($"Subject: {email.Subject}"); // subject: Webhookのテスト
context.Logger.LogLine($"Filter: {email.Filter}"); // filter: info@smtps.jp
context.Logger.LogLine($"ServerComposition: {email.ServerComposition}"); // server_composition: sandbox
context.Logger.LogLine($"EnvelopeFrom: {email.EnvelopeFrom}"); // envelope_from: info@smtps.jp

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

using Amazon.Lambda.Core;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

// 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 ApiGatewayV2HttpEvent
{
    public string Body { get; set; }
    public bool IsBase64Encoded { get; set; }
}

public class EmailWebhookEvent
{
    [JsonPropertyName("filter")]
    public string Filter { get; set; }
    [JsonPropertyName("headers")]
    public List<EmailHeader> Headers { get; set; }
    [JsonPropertyName("subject")]
    public string Subject { get; set; }
    [JsonPropertyName("envelope-to")]
    public string EnvelopeTo { get; set; }
    [JsonPropertyName("server_composition")]
    public string ServerComposition { get; set; }
    [JsonPropertyName("html")]
    public string Html { get; set; }
    [JsonPropertyName("text")]
    public string Text { get; set; }
    [JsonPropertyName("envelope-from")]
    public string EnvelopeFrom { get; set; }
}

public class EmailHeader
{
    [JsonPropertyName("name")]
    public string Name { get; set; }
    [JsonPropertyName("value")]
    public string Value { get; set; }
}

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(ApiGatewayV2HttpEvent request, ILambdaContext context)
    {
        var email = JsonSerializer.Deserialize<EmailWebhookEvent>(request.Body);
        context.Logger.LogLine($"Subject: {email.Subject}");
        context.Logger.LogLine($"Filter: {email.Filter}");
        context.Logger.LogLine($"ServerComposition: {email.ServerComposition}");
        context.Logger.LogLine($"EnvelopeFrom: {email.EnvelopeFrom}");
        return "ok";
    }
}

Lambdaのデプロイ

コードはCLIでZip化します。

cd src/ExampleCS
dotnet lambda deploy-function ExampleCS

これで bin/Release/net8.0ExampleCS.zip が作成されます。これをAWS Lambdaにアップロードします。

Webhookの結果は管理画面で確認

Webhookでデータが送信されたログは管理画面で確認できます。送信時のAPIキー設定など、HTTPヘッダーを編集するといった機能も用意されていますので、運用に応じて細かなカスタマイズが可能です。

Webhookログ

まとめ

メールと連携したシステムはよくあります。通常、メールサーバを立てて、その中で処理することが多いのですが、メールサーバが落ちてしまうとシステムが稼働しなくなったり、メール文面の解析が煩雑でした。Customers Mail Cloudを使えばそうした手間なくJSONで処理できて便利です。

受信サーバ | Customers Mail Cloud