Customers Mail CloudのWebhookは2種類あります。
- メール受信時
- メール送信時
メール受信時のWebhookはその名の通り、メールを受け取った際に任意のURLをコールするものです。この記事では添付ファイル付きメールを受け取った際のWebhook処理について解説します。
フォーマットはマルチパートフォームデータ
Webhookの形式として、JSONとマルチパートフォームデータ(multipart/form-data)が選択できます。この二つの違いは、添付ファイルがあるかどうかです。JSONの場合、添付ファイルは送られてきません。今回のようにメールに添付ファイルがついてくる場合は、後者を選択してください。

送信されてくるデータについて
メールを受信すると、以下のような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') }
マルチパートフォームデータを取得する
添付ファイルを処理する際には tempfile と base64 というライブラリを利用します。まず、ライブラリをインポートします。
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
これで、処理結果を fields や files で取得できます。
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ヘッダーを編集するといった機能も用意されていますので、運用に応じて細かなカスタマイズが可能です。

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