Technical document

SendGrid Inbound Parseで受信メールを業務データにする設計

メールの本文や添付をアプリで扱うには、メールを受け取る入口と、業務で使うデータを返す出口を分けて考えます。SendGrid Inbound Parseはその入口を担います。本稿では公開仕様をもとに、原本を失わず、失敗後にも解析をやり直せる構成を設計例として説明します。特定システムの実装完了や実測性能を報告する記事ではありません。
メール連携 / データ設計約12分公開日 2026年9月10日更新日 2026年9月10日
メールを原本保管庫へ保存し、整理したデータへ変換する流れを表すイラスト

Summary

この文書の要点

  • Inbound Parseが行うのは受信メールのHTTP POST。アプリ向けのJSONと取得APIは自分たちで設計します。
  • 署名を検証した原本と再処理用の受付記録を永続保存してから、受付成功を返します。
  • 再配送と解析の再実行を想定し、原本・解析結果・案件への所属を分けます。
  • 返信ヘッダーだけを信用せず、参加者と取込経路も確認し、判断できない記録は未整理に残します。

Inbound Parseは、受信メールをWebhookへ届ける入口

Inbound Parseは、設定した受信用ホストへ届いたメールの内容を、指定したURLへHTTP POSTする機能です。アプリがSendGridへ問い合わせて受信一覧を取得する方式とは異なります。Webhookへの成功応答は受け付けたことを知らせるもので、メール送信者に解析結果が返信されるわけではありません。

通常の解析形式では、件名、差出人、宛先、テキスト本文、HTML本文、ヘッダー、配送上の宛先情報、添付などを受け取れます。添付を含むmultipart/form-dataを扱うため、受信処理をrequest.json()だけで実装しないことが出発点です。

rawモードではemailフィールドに完全なMIMEメッセージを受け取る構成を選べます。通常形式とraw形式では届く項目が異なるため、両方の本文・添付フィールドが常に揃うとは仮定しません。原文の確認や将来の再解析を重視する本稿の設計例ではrawモードを選び、保存後にMIMEを解析します。

普段の受信箱を残し、取込用の経路を追加する

受信用サブドメインのMXをSendGridへ向け、同じホスト名とWebhook URLをInbound Parseに登録します。日常利用する受信箱を維持する場合は、取込用のサブドメインだけを追加し、必要なメールをそこへ転送します。既存の受信ドメイン全体のMXを置き換える必要はありません。

送信記録は専用取込アドレスへのBccでも保存できます。ただし、この記録は送信メールの控えであり、相手への到達や開封の証明ではありません。受信経路と送信用アドレスの登録情報から方向を判定し、Fromの表示だけで送信済みと扱わない設計にします。メール送信機能には別のサービスを利用する構成も可能です。

本番と検証環境では受信用ホスト、Webhook、保存先を分離します。転送設定の変更前に、既存の転送先から取込先へ戻る循環がないか確認し、最初は合成メールで到達を確かめます。

署名検証は、multipartを解析する前に行う

Inbound Parseには署名検証やOAuthを設定するsecurity policyがあります。署名方式を使うなら、ポリシーを作るだけでなく対象Webhookへ関連付け、発行された公開鍵を受信側へ配置します。URLを公開しただけでリクエストが認証されるわけではありません。

署名検証には受け取ったままのHTTPボディを使います。multipartを先に分解し、ファイルや改行を組み直すとバイト列が変わり、検証に失敗します。署名ヘッダーとtimestampを公式方式で検証し、その後で内容の解析に進みます。

Webhookの署名が証明するのは配送元との通信です。元メールの差出人が本物であることや、案件を閲覧・更新する権限までは証明しません。メールの認証結果、参加者、取込経路の検査は別に行います。リプレイ対策も必要ですが、正規の再送を拒否しないようtimestampの扱いと再送時の挙動を検証します。

原本と受付台帳を保存してから成功を返す

構成例は、SendGrid → 受付Worker → 非公開R2とD1の受付台帳 → Queues → 解析Worker → 閲覧APIです。受付Workerではサイズ制限と署名検証を行い、受信した原本を非公開領域へ保存します。D1には原本の参照、処理状態、再試行情報とキュー投入待ちの記録を持たせます。

R2への保存、D1の更新、Queuesへの送信を一つのトランザクションにはできません。そこでR2保存後に、受付台帳と投入待ち記録をD1内の同一トランザクションで確定します。ここまで成功して初めて2xxを返す構成にします。キュー送信前に停止しても、定期処理が投入待ち記録を拾って回復できることが条件です。

R2保存後にD1保存が失敗した場合は、成功を返さず再配送に備えます。残った未参照オブジェクトは、受付記録との照合と保持期間を経て整理します。キュー送信の直後、投入済み記録を更新する前に停止すれば重複投入が起こるため、解析側でも同じ処理を重ねない仕組みが必要です。

SendGridの公式説明では2xxで再送が停止し、5xxへの再試行と、配送できないメールを3日後に破棄する扱いが示されています。外部サービスの再送だけを回復策にせず、受付障害を検知する監視と、保存済みジョブを再実行する経路を用意します。

  • 保存前の一時障害:5xxで再試行を求め、失敗率を監視する。
  • 保存後の解析失敗:原本を維持し、再試行回数と失敗コードを受付台帳へ記録する。
  • キュー投入漏れ:投入待ち記録を定期的に走査する。処理中のまま止まったジョブも期限付きの占有で回収する。
  • 再試行上限:隔離またはDead Letter Queueへ移し、担当者が原因を確認して再投入する。

原本・解析結果・業務上の所属を分けて持つ

原本は保存したMIMEと添付の出所を確認する基準です。解析結果はそこから作る検索・表示用データなので、解析器の修正後に作り直せます。案件への所属は人が変更できる業務情報として別に保持します。この境界があれば、案件の付け替えで原文や送受信者が変わりません。

下のJSONはアプリ側で設計する取得APIの応答例です。SendGridのペイロードそのものではありません。認証・閲覧権限を確認したAPIが返し、本文や添付は必要な画面で取得します。未解析状態を空本文と混同しないよう、状態を明示します。

  • receivedAt:受信基盤が記録した日時。messageDate:メール記載日時。解析不能なDateはnullにし、原文を残す。
  • Message-ID、In-Reply-To、References:元のヘッダーを保存し、検索用の解析値を別に持つ。
  • envelopeの宛先とヘッダー上のTo・Cc:配送先と表示先を区別する。Bccの取込先がToにあるとは限らない。
  • 添付:原本参照、ファイル名、申告MIME型、検査結果、サイズを保持し、保存先を公開URLにしない。
  • 案件への所属:変更版、操作者、変更前後、理由を監査履歴へ保存する。
アプリ側のメール取得APIが返すデータ例(架空の識別子)
{
  "id": "mail_demo_001",
  "parseStatus": "parsed",
  "parserVersion": 1,
  "direction": "unknown",
  "subject": "点検日程の確認",
  "receivedAt": "2026-09-10T01:02:03Z",
  "messageDate": "2026-09-10T00:58:00Z",
  "caseId": null,
  "replyToRecordId": null,
  "attachmentCount": 1,
  "originalAvailable": true
}

重複排除と返信の関連付けは、別の判定にする

Cloudflare Queuesはat-least-once配送なので、同じジョブが複数回来る前提で実装します。受付IDと解析器の版に一意制約を設け、結果の保存と処理完了の更新を原子的に行います。通知などの副作用にも独立した重複防止キーが必要です。

Webhookの再配送と、利用者が同じメールをもう一度転送した場合は区別します。Message-IDは欠落や偽装、再利用があり得るため、それだけを全体共通の一意キーにはしません。取込先の境界、配送経路、原本のハッシュを使って照合し、同一メールか断定できない受信は配送記録として残します。multipartの境界や転送ヘッダーが変わる場合もテストします。

既存案件への自動追加は、返信ヘッダーが既知の記録を指し、参加者と取込経路も確認できた場合に限定します。複数案件を指す場合や、手動転送で返信関係が失われた場合は未整理に残します。件名の一致や同じ差出人という理由だけでは統合しません。

手動での所属変更は権限を検証し、変更と監査履歴を同時に保存します。更新版が一致しなければ競合として戻します。メールの移動に伴って金額やタスクを移したり、送信控えの取込だけで対応完了にしたりしないことも、データ設計の一部です。

表示用の加工をしても、原文へ戻れるようにする

HTMLメールは未信頼の入力です。画面にはサニタイズした内容を表示し、スクリプトや危険なURLを除去します。外部画像は既定で読み込まず、開封に伴う情報送信を避けます。引用文の折りたたみは表示用の派生処理にとどめ、切り出しを誤っても原文を確認できるようにします。

添付の拡張子やContent-Typeを信用して直接開かず、種類、実サイズ、圧縮ファイルの展開量、マルウェアを検査します。未検査のファイルは隔離し、取得のたびに閲覧権限を検証します。原文のダウンロードも認証の対象です。

Workersで受信可能なHTTPサイズとメモリ、R2への書込方法、MIME解析器の消費メモリは別々に確認します。rawボディと解析結果を同時に全量保持すると、大きな添付でメモリを使い切ることがあります。上限を事前に定め、その上限付近で署名検証から保存までを測ります。

運用ログには受付ID、状態、処理時間、失敗コードを記録し、本文、メールアドレス、添付、認証情報を出しません。解析エラーに原文が混ざらないかも確認します。取り込んだメールをアクセス解析や記事生成AIへ自動転送せず、保存期間と削除時の派生データの扱いを決めます。

導入前は、成功例よりも停止位置を変えて検証する

本文が表示できるテストだけでは、メールを失わないことは確かめられません。保存やキュー送信の前後で意図的に処理を止め、受付記録から回復できることと、同じ記録や通知が二重に増えないことを確認します。以下はこの構成を実装するときの検証項目であり、本稿での実機検証結果ではありません。

  • 入力形式:日本語と複数の文字コード、textのみ、HTMLのみ、添付、インライン画像、壊れたMIME、上限付近のメール。
  • 認証:署名なし、ボディ改変、不正な鍵、鍵の切替、正規の再送、差出人の偽装。
  • 障害:R2保存後・D1確定前、D1確定後・キュー送信前、解析結果保存前後で停止して回復を確認。
  • 関連付け:通常返信、手動転送、同じ件名の別案件、複数候補、同時の手動変更、権限外の案件。
  • 運用:送信Bcc、転送ループ、自動返信、迷惑メール、未整理の滞留、再試行上限と再投入。
  • 取得API:権限外の原文・添付を取得できないこと、ページ送りと索引、未解析状態の表示、本文のログ混入。

データ化の基準は、後から説明し直せること

受信時に全処理を終える構成は小さく始めやすい一方、添付検査や外部処理が遅いと再配送を招きます。保存と解析を分ける構成では受付を短くできますが、処理待ちの表示、回復ジョブ、監視を自分たちで運用する必要があります。必要な規模と回復要件に応じて選びます。

メールのデータ化で残したいのは、本文の文字列だけではありません。どの原本から作ったか、いつ受け取ったか、なぜ案件へ関連付けたかを辿れることが、誤解析や誤操作を修正する土台になります。入口では原本を確保し、出口では権限を確認した必要なデータを返す。この二つを分けると、メール連携を業務の変更に追従させやすくなります。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ