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

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で新しい関数を作成します。その際、条件として以下を指定します。

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

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

require 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('ok') }
end

Rubyのコード

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

import json

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

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

添付ファイルを処理する際には tempfilebase64 というライブラリを利用します。まず、ライブラリをインポートします。

require 'tempfile'
require 'base64'

メールのボディ部分をマルチパートフォームデータとして parse_multipart 関数で解析します。ファイル名がエンコードされている場合には、 decode_mime_encoded_string 関数にてデコード処理を行います。

def decode_mime_encoded_string(encoded_string)
    if encoded_string =~ /=\?UTF-8\?B\?(.+)\?=/
        Base64.decode64($1).force_encoding('UTF-8')
    else
        encoded_string
    end
end
  
def parse_multipart(body, content_type)
    # Content-Typeからboundaryを取得
    boundary_match = content_type.match(/boundary=(.+)/)
    return {} unless boundary_match

    boundary = boundary_match[1]
    parts = body.split("--#{boundary}")

    data = {}

    parts.each do |part|
        next if part.strip.empty? || part.strip == "--"

        # ヘッダーとデータ部分を分ける
        header, content = part.split("\r\n\r\n", 2)
        next unless header && content

        # ヘッダー解析
        name_match = header.match(/name="([^"]+)"/)
        filename_match = header.match(/filename="([^"]+)"/)
        content_type_match = header.match(/Content-Type: (.+)/)
        puts "name_match -> #{name_match}, filename_match -> #{filename_match}"

        if filename_match
            decoded_filename = decode_mime_encoded_string(filename_match[1])  # ← デコード処理を追加
            # ファイルデータ
            temp_file = Tempfile.new(decoded_filename)
            temp_file.binmode # バイナリモードにする
            temp_file.write(content.force_encoding('ASCII-8BIT')) # そのまま書き込む
            temp_file.rewind

            data[name_match[1]] = {
                filename: decoded_filename,
                content_type: content_type_match ? content_type_match[1] : 'application/octet-stream',
                tempfile: temp_file
            }
        else
            # 通常のフォームデータ(文字列)
            data[name_match[1]] = content.force_encoding('UTF-8').strip
        end
    end

    data
end

そして、受け取ったデータに対して、マルチパートの解析を行います。

def lambda_handler(event:, context:)
  content_type = event['headers']['content-type']
  body = event['body']
  # base64エンコードされている場合はデコード
  if event['isBase64Encoded']
      body = Base64.decode64(body)
  end
  # マルチパートフォームデータを解析
  parsed_data = parse_multipart(body, content_type)
end

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

puts parsed_data["server_composition"] # pro
puts parsed_data["filter"] # info@smpts.jp
puts parsed_data["attachment1"][:filename] # 添付ファイル名

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

parsed_data["attachment1"][:tempfile].path # /tmp/ファイル名

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

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

Webhookログ

まとめ

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

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

受信サーバ | Customers Mail Cloud