Webhook APIを使って添付ファイル付きメールを処理する(AWS Lambda + Python)

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

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

メール受信時のWebhookはその名の通り、メールを受け取った際に任意のURLをコールするものです。この記事では添付ファイル付きメールを受け取った際のWebhook処理について解説します。

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

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

Webhook設定ダイアログ

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

メールを受信すると、以下のようなWebhookが送られてきます(データは一部マスキングしています)。JSONにしていますが、実際にはmultipart/form-dataです。

{
    "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",
    "attachments": 1,
    "attachment1": "...."
}

AWS Lambdaの準備

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

  • ランタイム
    Python 3.13
  • 関数 URL を有効化
    チェックを入れる
  • 認証タイプ
    NONE

関数の作成が完了したら、関数コードを編集します。ベースになるコードは以下の通りです。

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps('ok')
    }

Pythonのコード

処理は lambda_function.py に記述します。

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps('ok')
    }

マルチパートフォームデータを取得する

添付ファイルを処理する際には requests-toolbelt というライブラリを利用します。

pip install requests-toolbelt -t .

そしてライブラリをインポートします。

import base64
from requests_toolbelt.multipart import decoder
from email.header import decode_header, make_header

最初にマルチパートのデータをデコード処理します。

# Content-Type ヘッダーの取得
content_type = event['headers'].get('Content-Type') or event['headers'].get('content-type')
# Content-Type が multipart/form-data であることを確認
if content_type and content_type.startswith('multipart/form-data'):
    # リクエストボディの取得とデコード
    body = event['body']
    if event.get('isBase64Encoded', False):
        body = base64.b64decode(body)
    else:
        body = body.encode('utf-8')
    
    # マルチパートデータの解析
    multipart_data = decoder.MultipartDecoder(body, content_type)

そして、 multipart_data に対してフィールドとファイルの取得を行います。

fields = {}
files = []

for part in multipart_data.parts:
    content_disposition = part.headers.get(b'Content-Disposition', b'').decode('utf-8')
    if 'filename' in content_disposition:
        # ファイルデータの処理
        file_data = part.content
        # ファイル名を取得
        filename = content_disposition.split('filename=')[1].strip('"')
        decoded_filename = decode_rfc2047(filename)
        files.append({
            'filename': decoded_filename,
            'content': file_data
        })
        print(f"Received file: {decoded_filename} ({len(file_data)} bytes)")
    else:
        # フォームフィールドの処理
        field_name = content_disposition.split('name=')[1].strip('"')
        field_value = part.text
        print(f"Received field: {field_name} = {field_value}")
        fields[field_name] = field_value

ファイル名はデフォルトでエンコードされています。そこで、 decode_rfc2047 という関数を作って、デコードします。

def decode_rfc2047(encoded_string):
    # エンコードされた文字列をデコード
    decoded_tuple = decode_header(encoded_string)
    # デコードされた部分を結合して完全な文字列を作成
    decoded_string = str(make_header(decoded_tuple))
    return decoded_string

これで、処理結果を fieldsfiles で取得できます。

print(fields['server_composition']) # pro
print(files[0]['filename'])         # 稟議書.docx

ファイルを保存する場合には、 content にデータが入っています。

console.log(files[0]['content']); // ファイルの内容

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

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

Webhookログ

まとめ

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

添付ファイルまで処理対象にしたい時には、この方法を利用してください。

受信サーバ | Customers Mail Cloud