はじめに
KaiMailは、独自ドメインのメールをHTTP Webhookとして受信できるメール転送サービスである。MXレコードを設定するだけで、そのドメイン宛のメールがJSON形式のPOSTリクエストとしてアプリケーションに届く。メールサーバーの運用は不要だ。
この記事では、KaiMailのWebhook連携でメールをプログラム処理する方法を、Flaskのコード例とともに説明する。
メールをプログラムで受信するのは面倒
開発者というのは、アイデアが浮かぶたびにドメインを取得してしまう生き物である。私の周りの開発者もそうだし、私自身もそうだ。ドメインにウェブサイトを載せるのは簡単で、今はホスティングサービスがいくらでもある。問題はメールの受信だ。
自前でPostfixやSendmailを立てるのは運用負荷が高い。そもそも自宅サーバーを誰でも立てることができた今は、ほとんどのプロバイダーは25番ポートを閉鎖してる。その上にSMTPの仕組みを理解している開発者は、Node.jsやReactを使える開発者より圧倒的に少なくなった。GmailやOutlookのおかげでメールの仕組みを意識する機会がなくなったからだろう。
Amazon SESやSendGrid Inbound Parseのようなクラウドサービスもあるが、設定が複雑で、メール受信だけのために使うにはオーバースペックなことが多い。
開発者が本当に求めているのは、こういうシンプルな体験だ。「このアドレスにメールが届いたら、その内容でHTTPエンドポイントを叩いてくれ。」
ユースケースは幅広い。サポートチケットの自動作成、請求書や領収書の処理、IoTデバイスからのアラート通知、社内の通知ルーティングなど、メール受信をトリガーにしたい処理は意外と多い。
KaiMailのWebhook連携の仕組み
全体のフローはシンプルである。
- 送信者がメールを送る
- KaiMailのMXサーバーがメールを受信する
- KaiMailが指定されたHTTPSエンドポイントにPOSTリクエストとしてJSONペイロードを配信する
必要なものは3つだ。
- KaiMailのPLUSプラン以上のアカウント
- カスタムドメイン(DNSのMXレコードを
mail.kaimail.netに向ける) - Webhook配信先となるHTTPSエンドポイント
KaiMailの管理画面でメールボックスを作成し、配信方法を「Webhook」に設定してエンドポイントURLを入力するだけで準備は完了する。詳細なセットアップ手順は公式ドキュメントを参照してほしい。
ペイロードの構造
KaiMailがWebhookで送信するJSONペイロードは以下のような構造になっている。
{
"version": "1.0",
"timestamp": "2025-01-31T10:30:00Z",
"headers": {
"From": "alice@example.com",
"To": "test@yourdomain.com",
"Subject": "Quick question",
"Date": "Fri, 31 Jan 2025 10:30:00 +0000",
"Message-ID": "<abc123@example.com>"
},
"body_text": "Hi,\n\nDo you offer annual discounts?\n\nThanks,\nAlice",
"body_html": null,
"attachments": [],
"envelope": {
"sender": "alice@example.com",
"recipient": "test@yourdomain.com"
},
"account": {
"email": "you@example.com",
"domain": "yourdomain.com",
"mailbox": "test@yourdomain.com"
},
"metadata": {
"authentication_results": {}
}
}
主要なフィールドを見ていこう。
-
headers: 元のメールヘッダーがそのまま入る。
Receivedのように複数値を持つヘッダーは配列として格納される。 -
body_text / body_html: プレーンテキストとHTML本文。テキストのみのメールでは
body_htmlがnullになり、HTMLのみならbody_textがnullになる。マルチパートメールでは両方が存在する。 -
attachments: 添付ファイルの配列。
filename、content_type、size、url(プリサインURL、24時間で期限切れ)を持つ。本体はJSONに埋め込まれない。 -
envelope: SMTPエンベロープの送信者と受信者。
From/Toヘッダーと異なる場合がある。 - metadata: SPF、DKIM、ARCなどのメール認証結果。
Flaskで受信エンドポイントを作る
実際にWebhookを受信するFlaskアプリケーションを書いてみよう。まず最小限のバージョンから始める。
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def receive_webhook():
payload = request.get_json()
subject = payload.get("headers", {}).get("Subject", "(no subject)")
sender = payload.get("headers", {}).get("From", "(unknown)")
body = payload.get("body_text", "")
print(f"From: {sender}")
print(f"Subject: {subject}")
print(f"Body: {body[:200]}")
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
これだけでWebhookの受信は動くが、本番で使うなら署名検証が必須である。KaiMailはルートに設定されたWebhookシークレットを使い、HMAC-SHA256ですべてのペイロードに署名する。署名はX-KAI-Webhook-Signatureヘッダーにsha256=<hex_digest>の形式で送信される。
署名検証を追加した完全なバージョンはこうなる。
import hashlib
import hmac
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("KAIMAIL_WEBHOOK_SECRET", "")
def verify_signature(payload_bytes, secret, received_signature):
"""KaiMailのWebhook署名を検証する"""
expected = hmac.new(
secret.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
received_hex = received_signature.removeprefix("sha256=")
return hmac.compare_digest(expected, received_hex)
@app.route("/webhook", methods=["POST"])
def receive_webhook():
# 署名検証
signature = request.headers.get("X-KAI-Webhook-Signature", "")
if not verify_signature(request.get_data(), WEBHOOK_SECRET, signature):
abort(403)
payload = request.get_json()
# トラッキングIDで重複チェック(実装は省略)
tracking_id = request.headers.get("X-KAI-Tracking-ID", "")
subject = payload.get("headers", {}).get("Subject", "(no subject)")
sender = payload.get("headers", {}).get("From", "(unknown)")
print(f"[{tracking_id}] From: {sender} / Subject: {subject}")
# ここで実際の処理を行う(チケット作成、DB保存など)
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
環境変数KAIMAIL_WEBHOOK_SECRETにKaiMail管理画面で表示される署名シークレットを設定して起動する。
export KAIMAIL_WEBHOOK_SECRET="your_signing_secret_here"
python app.py
署名検証のポイント
Webhookエンドポイントは公開URLなので、署名を検証しないと偽のメールデータを注入される可能性がある。実装上の注意点が2つある。
1つ目はsha256=プレフィックスの除去だ。ヘッダーの値はsha256=abc123...という形式だが、hmac.new().hexdigest()はabc123...のみを返す。プレフィックスを除去せずに比較すると検証は常に失敗する。(実際、私たち自身のサンプルコードでまさにこのバグを見つけた。)
2つ目はhmac.compare_digestの使用だ。通常の==はタイミング攻撃に脆弱だが、hmac.compare_digestは定数時間で比較するためこの攻撃を防げる。
本番環境で気をつけること
Webhookを本番で運用する際に押さえておくべきポイントをまとめる。
即座に200を返し、処理は非同期で行う。 受信したらDBに保存して200を返し、実処理はバックグラウンドジョブで実行する。処理が遅いとタイムアウトでリトライが発生する。
X-KAI-Tracking-IDで重複を排除する。 配信ごとにユニークなIDが付与される。リトライで同じWebhookが複数回届く可能性があるので、このIDで処理済みかどうかをチェックする。
リトライの挙動を理解する。 5xx、408、429の場合、KaiMailは5秒、10秒、20秒の間隔で最大3回リトライする。その他の4xxはリトライされない。
HTTPSは必須である。 KaiMailはSSL証明書を検証する。開発中はngrokを使ったローカルテストが便利だ。
添付ファイルのURLは24時間で期限切れになる。 受信したらすぐにダウンロードして自前のストレージに保存すること。
まとめ
メールをプログラムで受信するのは、本来もっとシンプルであるべきだ。KaiMailのWebhook連携を使えば、メールサーバーの運用やSMTPの知識なしに、HTTPエンドポイントひとつでメール受信を実現できる。
この記事ではFlaskでの実装例を紹介したが、HTTP POSTを受け取れるフレームワークなら何でも対応できる。Express、FastAPI、Gin、何でもいい。
詳しい設定手順やペイロードの全仕様はWebhook連携ガイドを、ローカル環境でのテスト方法はPythonでKaiMailのWebhookをテストする方法を参照してほしい。
