【サーバーレスWebhook入門】独自ドメインのメールをCloudflare Workersで受信する
KaiMailのWebhookをCloudflare Workersで受信する方法を解説。サーバーレス構成で独自ドメインのメールをJSON化し、メンテナンス不要の自動化基盤を構築する
「メールが届いたら何かしたい」という要望は、意外と古くからある。注文確認メールを受け取って在庫を更新する、問い合わせを受け付けてチケットを作成する、請求書を受信して会計システムに取り込む。こうした自動化の壁は、メールサーバーの管理にある。メールを受信するためだけにサーバーを立てて、OSを更新し、セキュリティパッチを当て、SSL証明書を更新する。そこまでする価値があるのか、と思う人も多いだろう。
メールを受信して処理したいけどサーバーは管理したくない、という人向けに、Cloudflare Workersを使ったサーバーレス構成を紹介する。メンテナンスは不要、コードは数行、最初の10万件までは無料だ。
今回の記事は、メールを受信する側にKaiMailのWebhookを利用して、そのWebhookがCloudflare Workersに向けてリアルタイムでJSONを渡す、という仕組みを簡単に説明したいと思う。
サーバーレスでメールを受信するという発想
従来のメール自動化は、だいたい次のどちらかだった。
一つ目は、メールサーバーを自分で構築する方法。PostfixやDovecotをインストールして、メールが届いたらプログラムを呼び出す。確かに自由度は高いが、メールサーバーの運用は面倒だ。Spam対策、認証設定、証明書管理、OSアップデート。小さなプロジェクトには荷が重すぎる。
二つ目は、メールボックスを定期的にポーリングする方法。GmailやOutlookのAPIを5分おきに叩いて、新着メールがないか確認する。これはサーバーが不要だが、リアルタイム性がない。メールが届いてから実際に処理されるまでに数分の遅延が生じる。APIのレート制限にも気を遣う必要がある。
Webhookはこの両方の欠点を回避する。メールが届いた瞬間に、指定したURLへHTTP POSTで通知が飛んでくる。リアルタイムであり、メールサーバーの管理も不要だ。ただ、Webhookを受信するためのエンドポイントが必要になる(結局サーバーも立たなければいけないが、メールサーバーではないから難易度はちょっと低い)。より簡単な方法としてここでCloudflare Workersが役に立つ。
Cloudflare Workersは、Cloudflareのエッジネットワーク上でJavaScriptやTypeScriptを実行できるサーバーレスプラットフォームだ。従来のサーバーレス関数(AWS Lambdaなど)と比べて、コールドスタートがほとんどない。グローバルに分散された250以上のデータセンターで実行されるため、どこからアクセスしても遅延が少ない。
Cloudflare Workersの料金と制限
個人プロジェクトや小規模な自動化なら、無料プランで十分だ。2026年7月現在の料金体系は以下の通り。
| 項目 | Freeプラン | Paidプラン($5/月〜) |
|---|---|---|
| リクエスト数 | 10万件/日 | 1,000万件/月(超過分は100万件あたり$0.30) |
| CPU時間 | 10ms/呼び出し | 3,000万ms/月(超過分は100万msあたり$0.02) |
| 実行時間 | 10ms | 最大5分(デフォルト30秒) |
| 同時実行数 | 無制限 | 無制限 |
メールWebhookの処理は、JSONをパースして外部APIを叩く程度の軽い処理だ。1件あたり数msで終わる。1日10万件の無料枠があれば、大多数のユースケースはカバーできる。実際、個人のプロジェクトで1日10万件のメールを処理することはまずないだろう。
Paidプランに移行しても、月$5の基本料金で1,000万件のリクエストが含まれる。メール自動化の規模が大きくなっても、予算はそう膨れ上がらない。
WorkersでWebhookを受信する仕組み
Cloudflare Workersは、HTTPリクエストを受信するとfetchイベントハンドラを呼び出す。KaiMailのWebhookは、メールが届くと設定したURLへ署名付きJSONをPOSTする。これをWorkersで受信して処理する。
基本的な流れは以下の通り。
- KaiMailダッシュボードでカスタムドメインを登録
- メールボックスの転送先にWebhook URLを設定 ― これはCloudflare Workersのデプロイ後に発行されるURL(
https://<your-worker>.workers.devなど)を、KaiMailダッシュボードの該当メールボックス設定に登録する - Cloudflare WorkersでそのURLを待ち受け
- JSONペイロードを受信して処理
KaiMailのWebhookペイロードには、メールの送信元、件名、本文、添付ファイルなどが含まれる。署名検証も可能だが、まずは最小構成から始める。
最小構成のWorkersコード
最初に、署名検証なしの最小構成を動かしてみる。動作を確認してから、本番向けのセキュリティ対策を追加するのが順序だ。
// index.ts
export default {
async fetch(request: Request): Promise<Response> {
// POSTリクエストのみ受け付ける
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
// KaiMailからのJSONペイロードを取得
const payload = await request.json();
// 必要な情報を抽出
const { from, to, subject, text, html } = payload;
// ここで実際の処理を行う
console.log(`受信: ${subject} (${from} → ${to})`);
// 成功レスポンスを返す(KaiMailが再送しないように)
return new Response('OK', { status: 200 });
},
};
これだけだ。フレームワークのインストールも、サーバーの起動も不要。Cloudflareのダッシュボードからコードを貼り付けるか、Wrangler CLIを使ってデプロイすれば、数十秒で世界中のエッジで動作し始める。
Wrangler CLIでのデプロイは以下の通り。
# プロジェクトの初期化
npx wrangler init kaimail-webhook --template hello-world
# コードを書き換えてデプロイ
cd kaimail-webhook
npx wrangler deploy
デプロイ後、WorkersのURL(https://<your-worker>.workers.dev)をKaiMailのWebhook設定に登録すれば、すぐに動作する。
実用的な処理パターン3選
Webhookを受信して「OK」と返すだけでは、実用的な自動化にはならない。ここからは、実際に使える処理パターンを3つ紹介する。
パターン1:特定の送信元からのメールをフィルタリング
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const payload = await request.json();
const { from, subject, text } = payload;
// 特定のドメインからのメールのみ処理
if (!from.endsWith('@example.com')) {
return new Response('Ignored', { status: 200 });
}
// 件名に「請求書」が含まれる場合のみ処理
if (subject.includes('請求書')) {
await env.KV.put(`invoice:${Date.now()}`, JSON.stringify({
from,
subject,
receivedAt: new Date().toISOString(),
}));
}
return new Response('OK', { status: 200 });
},
};
Cloudflare KVはキーバリューストアだ。請求書メールのメタデータを保存して、後からバッチ処理で一括ダウンロードする、といった使い方ができる。KVの無料枠は1日10万回の読み書きだ。
パターン2:外部APIに転送する
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const payload = await request.json();
// Slackへ通知
await fetch(env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `新着メール: ${payload.subject}\n送信元: ${payload.from}`,
}),
});
return new Response('OK', { status: 200 });
},
};
Workersから他のサービスへHTTPリクエストを送信するのは、追加料金なしで可能だ。Slack、Discord、LINE、自社APIなど、好きな宛先に転送できる。Workersからのサブリクエストは、メインのリクエストカウントには含まれない。
パターン3:署名検証を実装する
本番環境では、Webhookの署名検証を必ず実装すべきだ。KaiMailのWebhookには、ペイロードの改ざんを防ぐための署名が付いている。署名が正しいものでない場合、リクエストを無視する。
import { createHmac } from 'crypto';
async function verifySignature(
payload: string,
signature: string,
secret: string
): Promise<boolean> {
const expected = createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === `sha256=${expected}`;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const body = await request.text();
const signature = request.headers.get('X-KaiMail-Signature') || '';
if (!await verifySignature(body, signature, env.KAIMAIL_WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(body);
console.log(`検証済み: ${payload.subject}`);
return new Response('OK', { status: 200 });
},
};
署名検証は「余分な作業」のように思えるが、これがないと誰でもWebhookエンドポイントを叩いて偽のメールデータを注入できる。セキュリティは後回しにしない方がいい。ただし、最小構成でまず動作確認してから、署名検証を追加するのが現実的だ。
バッグったら
実装してみると、いくつかの落とし穴がある。
Workersがタイムアウトする
Cloudflare Workersのデフォルト実行時間は30秒だ。KaiMailはWebhookが200 OKを返すまで再送を繰り返す。Webhook処理の中で重い外部API呼び出しをしていると、30秒を超えてタイムアウトする可能性がある。
対策は2つある。一つは、重い処理を非同期に切り出すこと。Cloudflare QueuesやDurable Objectsを使えば、Webhook受信と実処理を分離できる。もう一つは、実行時間の上限を引き上げること。Cron TriggerやQueue Consumerの場合は15分まで延長できるが、通常のHTTPリクエストは5分が上限だ。
実際のところ、メールWebhookの処理は数秒で終わるはずだ。もし30秒を超えるなら、設計を見直した方がいい。
メールにファイルを添付されてる場合がある。KaiMailからのJSONデータはファイルをダウンロードできるURLもついてるのだが、大きいファイルだとダウンロード時間もかかるだろう。ここも注意してほしい。
デバッグが面倒
ローカルで動かしているサーバーと違い、Cloudflare Workersのログはダッシュボードで確認する必要がある。console.logの出力は、Wrangler CLIのwrangler tailコマンドか、Cloudflareダッシュボードのログストリームで確認できる。
# リアルタイムでログを確認
npx wrangler tail
ローカルでの開発は、wrangler devコマンドで可能だ。ただし、KaiMailからのWebhookはローカル環境に届かない(KaiMailはインターネット上のURLしか叩けない)。ngrokなどのトンネルサービスを使うか、開発用のWebhookテスト機能を使って、実際のメールではなくテストペイロードを送信するのが現実的だ。
JSONパースに失敗する
まれに、メール本文に不正な文字列が含まれる場合、request.json()が失敗することがある。この場合は、生のテキストとして受け取ってから手動でパースするか、エラーハンドリングを入れる。
let payload;
try {
payload = await request.json();
} catch (e) {
return new Response('Invalid JSON', { status: 400 });
}
KaiMailのWebhookペイロードはUTF-8でエンコードされているが、メール本文そのものは元のエンコーディングを保持している場合がある。これはメールの仕様上の制約だ。通常のテキストメールであれば問題ないが、特定のエンコーディングを使ったメールでは文字化けが生じる可能性がある。
従来の構成と比較する
最後に、サーバーレス構成と従来の構成を比較してみる。
| 項目 | サーバー構築(Postfix + Python) | ローカル開発(Flask + ngrok) | Cloudflare Workers |
|---|---|---|---|
| サーバー管理 | 必要(OS、パッチ、証明書) | 不要(開発マシン上) | 不要 |
| インターネット公開 | 必要 | ngrokが必要 | 自動(デプロイ後即公開) |
| スケーリング | 手動(サーバー追加) | 不可(1プロセス) | 自動(エッジで分散) |
| コールドスタート | なし(常時起動) | なし | ほぼなし(エッジ実行) |
| 費用 | VPS代($5〜/月) | 無料(開発のみ) | 無料〜$5/月 |
| SSL証明書 | 手動更新 | ngrokが管理 | Cloudflareが管理 |
| 署名検証 | 自前実装 | 自前実装 | 自前実装(コードは同じ) |
どの構成も一長一短がある。Postfixを自分で管理するのは面倒だが、最大の自由度がある。Flask + ngrokは開発や小規模運用には手軽だが、本番運用には向かない。Cloudflare Workersは本番運用に向いており、メンテナンスも少ないが、Cloudflareのプラットフォームにロックインされる。
個人的には、「メールを受信して何かしたい」というニーズの多くは、Cloudflare Workersで十分満たせると思っている。メールサーバーの管理に時間を取られるより、実際に解決したい問題に時間を使う方が建設的だ。
おさらいにすると、全体的像は異以下の感じ。
まとめ
KaiMailのWebhookをCloudflare Workersで受信することで、メンテナンス不要のメール自動化基盤が構築できる。最小構成は数行のコードで、デプロイは数十秒で完了する。最初の10万件は無料だ。
実装の順序としては、まず最小構成で動作確認をして、次に署名検証を追加し、最後に実用的な処理(外部API連携やKV保存)を実装するのがおすすめだ。一度基盤ができれば、新しい自動化フローの追加はWebhookエンドポイントに処理を追加するだけで済む。
サーバーレスのメリットは、インフラの管理から解放されることにある。メールを受信して処理したいだけなのに、メールサーバーの管理に週末を費やす必要はない。コードを書いてデプロイすれば、あとはCloudflareが面倒を見てくれる。
もちろん、プログラミング不要で既存サービスと連携したい場合は、Zapier連携ガイドも参考にしてほしい。Cloudflare Workersは開発者向けの柔軟な基盤だが、Zapierはノーコードで同様の連携が構築できる。
詳細なWebhookペイロードのスキーマや署名検証の仕様については、KaiMail Webhook連携ガイドを参照してください。Cloudflare Workersの詳細なAPIリファレンスは、Cloudflare Developersドキュメントを参照してください。
イクバル・アバドゥラ