記事一覧へ戻る
TRANSMISSION / CYAN読了 22 分

Cloudflare Email Service 送受信実践ガイド—Routing、Workers、API、SMTP まで

2026 年 7 月時点の Cloudflare Email Service を使った受信、転送、解析、返信、トランザクションメール送信を、Nuxt、Workers、D1、セキュリティ、レート制限とともに解説します。

#Cloudflare#Email#Workers#Serverless

解説動画 / 16:9

Cloudflare Email Service 実践—11 分で分かる設定ガイド

DNS、Email Routing、Email Worker、send_email binding から REST、SMTP、Nuxt、D1、5 分間の認証コード、本番確認までを解説します。

11:01中国語ナレーション12 チャプター

再生ボタンから完全版をご覧いただけます。シーク、ピクチャーインピクチャー、全画面表示に対応しています。

長い間、Cloudflare とメールの関係は主に二つでした。DNS を管理することと、Email Routing で support@example.com を実際のメールボックスへ転送することです。

2026 年、この境界は大きく前へ進みました。

Cloudflare は 4 月に Email Sending を public beta へ移行し、Workers から env.EMAIL.send() を直接呼んでトランザクションメールを送れるようにしました。6 月には認証 SMTP も追加されました。従来の Email Routing と Email Workers を組み合わせると、Cloudflare Email Service は現在、次の処理を行えます。

  • 独自ドメイン宛てのメールを受信する。
  • 検証済みの実メールボックスへ転送する。
  • 受信メールを Worker に渡し、解析、フィルター、保存、自動化を行う。
  • 元のメールスレッド内で返信する。
  • Worker、REST API、SMTP からトランザクションメールを送る。
  • 送信ログ、bounce、suppression、配信指標を確認する。

この記事は 2026 年 7 月 28 日時点の公式機能を基準に、Cloudflare のメール送受信モデル全体を整理し、Nuxt、Workers、D1 プロジェクトに適した実装方法を示します。

先に結論を述べます。

Cloudflare Email Service は従来型の IMAP / POP3 メールボックスでも、Web 画面でメールを読む企業向けスイートでもありません。「メールの入口、送信チャネル、プログラム可能なイベント」の集合として捉えると分かりやすく、認証コード、パスワードリセット、注文通知、システムアラート、サポートチケット、メール駆動の自動化に適しています。

最初に「受信」と「送信」を分ける

メールの送受信は一つの機能に見えますが、Cloudflare では独立した二つの経路です。

方向Cloudflare 製品入口Worker でできること
受信Email Routing / Email Workers外部メールサーバーが Cloudflare MX へ接続転送、返信、拒否、解析、D1/R2 への保存、Queue への投入
送信Email SendingWorkers binding、REST API、認証 SMTPtext/HTML、添付、カスタム header を送り、messageId を取得

受信経路はおおよそ次のとおりです。

送信元 SMTP
  → Cloudflare MX
  → SPF / DKIM / DMARC と reputation の確認
  → アドレスルールまたは Catch-all に一致
  → メールボックスへ転送 / Worker / drop

送信経路は次のとおりです。

Nuxt API / Worker / 外部アプリケーション
  → EMAIL binding / REST API / SMTP
  → 送信ドメインとパラメータの検証
  → Cloudflare 配信パイプライン
  → 受信側メールサーバー

メールはエッジ入口で検査され、実メールボックス、Worker 処理ノード、拒否経路へ振り分けられる

これは概念図です。実際の判断には SMTP セッション、メール認証、ルールマッチ、Worker 実行制限も含まれます。

2026 年の製品境界と料金

Cloudflare は現在、受信と送信を Email Service の下にまとめていますが、利用できるプランは異なります。

Email Routing:受信と転送

Email Routing は Workers Free と Workers Paid の両方で利用できます。公式料金ページでは受信メール数は無制限とされていますが、Worker が処理するメールは対応する Worker リクエストと計算資源を消費します。

適している用途は次のとおりです。

  • support@yourdomain.com を既存の個人メールボックスへ転送する。
  • サイトごとに独立した連絡先アドレスを作る。
  • orders+shop-a@yourdomain.com のような sub-address で送信元を区別する。
  • 受信メールを Worker でチケット、Webhook、データベースレコードへ変換する。

Email Sending:トランザクションメール

Email Sending は現在も public beta です。

任意の受信者への送信には Workers Paid プランが必要です。2026 年 7 月の公式料金では、

  • アカウントごとに月 3,000 通の送信が含まれる。
  • 超過後は 1,000 通あたり 0.35 ドル。
  • アカウント内の「検証済み宛先アドレス」へのメールは常に無料で、含まれる枠を消費しない。
  • Email Service に受理されたメールは、後で hard bounce しても通常は枠を消費する。API 境界で直接拒否されたリクエストは消費しない。

料金と beta 状態は今後も変わる可能性があります。本番導入前に公式料金ページを再確認し、この記事の数値を予算モデルへ固定しないでください。

第 1 歩:ドメインを正しく接続する

Email Service は Cloudflare DNS を利用するドメインを必要とします。送信と受信では用途の異なる二組の DNS record が作られます。

受信はルートドメインの record を使う

Email Routing を有効にすると、Cloudflare はドメインルートに次を設定します。

  • MX:受信メールを Cloudflare のメール入口へ届ける。
  • SPF:Cloudflare による転送を許可する。
  • DKIM:転送経路へ署名を付与する。

これらは「第三者が自分のドメインへメールを送る」経路を担当します。

送信は cf-bounce record を使う

Email Sending を有効にすると、Cloudflare は cf-bounce.yourdomain.com に次を設定します。

  • MX:bounce を処理する。
  • SPF:Cloudflare がドメインを代理して送信することを許可する。
  • DKIM:メール内容が転送中に変更されていないことを検証する。
  • _dmarc.yourdomain.com:認証失敗時の処理方針を宣言する。

送信と受信は異なる DKIM selector を使います。DKIM record が二つあっても重複設定ではありません。

Google Workspace、Microsoft 365、その他のメールサービスをすでに使っている場合は、MX と SPF の競合を特に確認してください。一つのドメインに有効な SPF policy は一つだけです。複数サービスを同時に許可する場合は include を統合し、二つ目の v=spf1 を作らないでください。

reputation の用途ごとに subdomain を分けることを勧めます。

noreply@example.com          アカウント認証とパスワードリセット
notify@mail.example.com      プロダクト通知
support@example.com          人によるサポートと受信処理

トランザクション通知、人とのやり取り、将来の一括メールが互いの reputation を損なわず、障害と移行の境界も明確になります。

メール受信:転送ルールから Email Worker まで

最も簡単な受信方法にはコードが不要です。

  1. Email Routing に検証済みの宛先メールボックスを追加する。
  2. support@yourdomain.com を作成する。
  3. 「メールへ送信」を選択する。
  4. Cloudflare が受信メールを宛先へ転送する。

件名で分類し、添付を抽出し、チケットを作り、自動返信したい場合は、action を「Worker へ送信」に変えます。

email() が受信メールの入口

Email Worker の入口は fetch() ではなく email() です。

interface Env {
  SUPPORT_FORWARD_TO: string
}

export default {
  async email(message, env): Promise<void> {
    const recipient = message.to.toLowerCase()
    const subject = message.headers.get('subject') ?? ''

    if (message.rawSize > 10 * 1024 * 1024) {
      message.setReject('Message is too large for this mailbox')
      return
    }

    if (recipient === 'support@example.com') {
      console.log('Support email accepted', {
        fromDomain: message.from.split('@').at(-1),
        subjectLength: subject.length,
        rawSize: message.rawSize,
      })

      await message.forward(env.SUPPORT_FORWARD_TO)
      return
    }

    message.setReject('Unknown recipient')
  },
} satisfies ExportedHandler<Env>

ここで messageForwardableEmailMessage です。よく使う field と操作は次のとおりです。

機能用途
message.fromSMTP envelope の送信者
message.toSMTP envelope の受信者
message.headersSubject、Message-ID などの header を読む
message.raw生の MIME stream
message.rawSize生メールのサイズ
message.forward(address)検証済み宛先へ転送する
message.reply(email)制約の範囲内で元の送信者へ返信する
message.setReject(reason)永続的な SMTP rejection を返す

見落としやすいセキュリティ上の注意があります。message.from / message.to は SMTP envelope の値で、メール本文 header 内の From: / To: とは異なる場合があります。認可、routing、audit では、ユーザーに表示する header アドレスを信頼できる identity と誤認しないでください。

本文が必要なときだけ MIME を解析する

転送またはアドレスによる routing だけなら、メール全体を解析する必要はありません。本文と添付を取り出す場合は postal-mime を使えます。

import PostalMime from 'postal-mime'

export default {
  async email(message): Promise<void> {
    const parsed = await PostalMime.parse(message.raw)

    console.log({
      subject: parsed.subject,
      textLength: parsed.text?.length ?? 0,
      attachmentCount: parsed.attachments.length,
    })
  },
} satisfies ExportedHandler

解析後の HTML、ファイル名、Content-Type、添付はすべて外部入力です。安全な方法は次のとおりです。

  • 管理画面へ受信 HTML を直接挿入しない。
  • 本文、添付数、展開後サイズを制限する。
  • R2 へ保存する前に新しい object key を生成し、元のファイル名を path に使わない。
  • メール本文から SQL、Shell、template、AI ツールパラメータを直接組み立てない。
  • virus scan、OCR、モデル呼び出しを受信イベントの同期 CPU 時間へ詰め込まず、重い処理は Queue へ渡す。

転送、返信、拒否、silent drop の違い

  • 転送:人のメールボックスで処理を続ける用途。宛先は事前に検証が必要。
  • 返信:自動確認やメール bot に適するが、追加制約がある。
  • 拒否:SMTP 段階で送信元へ受け付けなかったことを伝える。
  • drop:ルール一致後に処理を止め、送信元へ業務レベルの応答を返さない。

自動返信を任意の送信 interface として使うことはできません。現在の公式要件は次のとおりです。

  • 受信メールに有効な DMARC 結果がある。
  • 一つの受信イベントにつき返信は一度だけ。
  • 返信先は元の送信者である。
  • 返信の送信ドメインは受信したドメインと同じである。
  • メールループ防止のため、References が 100 項目を超えると返信を拒否する。

この制約はむしろ有益です。「このメールへ返信する」と「任意のアドレスへ新規メールを送る」が異なる権限になります。

メール送信:Workers binding を優先する

アプリケーションが Cloudflare Workers 上で動いているなら、Workers binding が最も自然です。業務コードに Cloudflare API Token を保存する必要がありません。

まず wrangler.jsonc で binding を宣言します。

{
  "send_email": [
    {
      "name": "EMAIL",
      "allowed_sender_addresses": [
        "noreply@example.com"
      ]
    }
  ],
  "vars": {
    "EMAIL_FROM": "noreply@example.com"
  }
}

allowed_sender_addresses は説明用の設定ではなく、実行時の権限境界です。endpoint が誤用されても、この binding は別の送信者アドレスになりすませません。

次の設定で宛先も制限できます。

  • destination_address:宛先を一つに固定する。
  • allowed_destination_addresses:宛先を許可リストに限定する。
  • allowed_sender_addresses:利用できる送信元アドレスを制限する。

システムアラート用 Worker は destination_address の固定に適しています。ユーザー認証サービスでは通常、送信者を制限し、アプリ側で宛先と頻度を検証します。

Worker 内から送信する

interface Env {
  EMAIL: SendEmail
}

export default {
  async fetch(_request, env): Promise<Response> {
    const result = await env.EMAIL.send({
      from: {
        email: 'noreply@example.com',
        name: 'Example App',
      },
      to: 'reader@example.net',
      subject: 'メールアドレスを確認してください',
      text: '認証コードは 482913 です。5 分間有効です。',
      html: '<p>認証コードは <strong>482913</strong> です。5 分間有効です。</p>',
    })

    return Response.json({
      messageId: result.messageId,
    })
  },
} satisfies ExportedHandler<Env>

Workers binding が成功すると messageId を返します。これは Cloudflare が送信リクエストを受理したことを意味し、ユーザーの受信トレイにすでに表示されたことを保証しません。最終配信、遅延、bounce、suppression は Email Service の log と metric で確認します。

Nuxt のサーバー API から送信する

Nuxt を Workers へデプロイすると、サーバールートから同じ binding を読めます。

import { z } from 'zod'

const schema = z.object({
  email: z.string().trim().email().max(254),
})

export default defineEventHandler(async (event) => {
  const { email } = schema.parse(await readBody(event))
  const env = event.context.cloudflare.env as {
    EMAIL: SendEmail
    EMAIL_FROM: string
  }

  // 実際のプロジェクトでは same-origin を確認し、
  // IP とメールアドレスを rate-limit し、D1 で
  // 一度の送信資格を atomically に取得します。
  const result = await env.EMAIL.send({
    from: {
      email: env.EMAIL_FROM,
      name: 'Example App',
    },
    to: email,
    subject: 'メールアドレスの確認',
    text: '認証コードは 5 分後に失効します。',
    html: '<p>認証コードは <strong>5 分後</strong>に失効します。</p>',
  })

  return {
    ok: true,
    messageId: result.messageId,
  }
})

この例を、匿名で無制限に呼べ、件名と本文を自由に指定できる公開 API にしないでください。自分のドメインが spam relay になります。

Worker が生成したトランザクションメールが複数の認証レイヤーを通り、異なる受信システムへ配信される

SPF、DKIM、DMARC は「誰が送信できるか、内容が変更されていないか、失敗時にどう処理するか」を扱います。すべてのメールがメイン受信トレイへ入ることは保証しません。

REST API と認証 SMTP が適する場面

Cloudflare は現在、三つの送信入口を提供しています。

方法最適な用途認証情報と境界
Workers bindingすでに Workers 上で動く Nuxt、Hono、Agentコードに API Token を保存せず、binding で送信者と宛先を制限できる
REST API別の cloud、CI、backend platform で動くプログラム最小権限 API Token を使い、response は受信者ごとに delivered、queued、permanent bounce を区別する
認証 SMTPNodemailer、PHPMailer、JavaMail などの SMTP client を使う既存システムsmtp.mx.cloudflare.net:465、implicit TLS のみ。password は Email Sending 権限を持つ API Token

認証 SMTP は 2026 年 6 月に追加された beta 機能です。現在は 465 port の SMTPS だけをサポートし、587 port の STARTTLS や匿名利用できる 25 port relay はありません。

SMTP Token が漏れると、所有者はアカウントへ接続された送信ドメインを使ってメールを送れます。そのため、

  • account-owned token を優先する。
  • 権限は Email Sending: Edit だけにする。
  • repository、image、frontend environment variable に書かない。
  • システムごとに別 Token を使い、個別に revoke できるようにする。
  • Worker binding を使えるなら、「SMTP に慣れている」という理由だけで長期 credential を追加しない。

必ず把握すべき制限

2026 年 7 月時点で、アプリ側のコードで事前に防御すべき主な platform limit は次のとおりです。

制限現在値実装への影響
ドメインごとの Email Routing ルール200multi-tenant システムでユーザーごとの静的ルールを作らない
アカウントごとの検証済み宛先200多数のメールボックスへ転送する前に account-level の宛先を設計
受信メールサイズ25 MiBそれより大きいメールは SMTP 段階で拒否
送信メール 1 通あたりの受信者50To、Cc、Bcc の合計
通常の送信メール総サイズ5 MiB添付と MIME encoding overhead を含む
検証済み宛先への総サイズ25 MiB検証済み宛先アドレスにのみ適用
カスタム header の総サイズ16 KB業務データを Header へ詰め込まない
1 Zone の送信/routing ドメイン数30root domain と subdomain の両方を数える
返信の References 項目数100超えると reply() が失敗

添付は Base64 と MIME で包装されます。3 MiB の生ファイルでも最終メールが 3 MiB のままとは限りません。encoding、本文、header の余裕を残してください。

platform は account reputation、配信成績、利用履歴に応じて一日の送信枠を調整することもあります。「月間枠が残っている」ことを、ある一秒間に無制限の並行送信ができるという意味にしないでください。

認証コードとパスワードリセットの設計

このブログの登録・パスワードリセットメールは、次の経路を使います。

ユーザーがメールアドレスを送信
  → same-origin check
  → IP とメールアドレスの rate limit
  → 6 桁コードを生成
  → D1 は salted hash、用途、有効期限、試行回数だけを保存
  → EMAIL binding で送信
  → 認証成功時に atomically に使用済みへ更新

重要な規則は次のとおりです。

  • 同じメールアドレスには 5 分に 1 通だけ送る。
  • 認証コードは 5 分後に失効する。
  • コードは一度だけ使用できる。
  • D1 に plaintext のコードを保存しない。
  • 連続して間違え、しきい値へ達したら認証を止める。
  • 送信失敗時は取得した枠を解放し、届かなかったコードでユーザーを lock しない。
  • パスワードリセット時は password hash だけを置き換えず、既存のログインセッションを revoke する。

rate limit はデータベース内で送信資格を atomically に取得する必要があります。前回送信時刻を検索してから row を insert する実装では、二つの並行リクエストが両方とも確認を通過します。unique key と INSERT ... ON CONFLICT ... WHERE を組み合わせ、どのリクエストが送信枠を得るかをデータベースに決めさせます。

メール API の response は「このアドレスが登録済みか」を公開してはいけません。攻撃者による user enumeration を可能にします。

より完全な本番アーキテクチャ

メール処理が Demo から本番へ進んだら、責務を分けます。

HTTP リクエスト / 業務イベント
  → 認証、origin check、入力検証、rate limit、idempotency
  → Queue
  → メール template の描画
  → EMAIL binding
  → messageId / error code を D1 へ保存
  → Email Service logs と alert

受信経路は次のようにできます。

support@yourdomain.com
  → Email Routing
  → Email Worker
  → 認証とサイズ確認
  → MIME 解析
  → 添付を R2 へ保存 / D1 にチケットを作成
  → 重い処理を Queue へ
  → 人のメールボックスへ転送、または制約付き返信

D1 に記録するもの

記録に適する項目は次のとおりです。

  • 業務イベント ID と idempotency key。
  • template ID と template version。
  • messageId
  • 受信者数。必ずしも完全なアドレスを保存する必要はない。
  • accepted / failed などアプリ側の状態。
  • Cloudflare error code。
  • 作成、送信、最終更新時刻。

既定では記録しないもの:

  • plaintext の認証コード。
  • パスワードリセット Token。
  • 完全なメール本文。
  • 添付内容。
  • SMTP/API Token。
  • デバッグのため無期限に保存する生の受信メール。

log 自体が新たな個人データ源になる可能性があります。

Email Routing の「Dropped」を送信失敗と判断しない

公式ドキュメントは、send_email binding から送ったメールが実際には正常に配信されていても、Email Routing の集計ページで dropped と表示される場合があると明記しています。

送信結果は Email Sending の Metrics と Logs で確認し、messageId を使ってアプリログと関連付けます。Routing ページが示すのは受信 routing の処理であり、完全な送信配信 state machine ではありません。

ローカルでのテスト

Wrangler は Email Worker のローカルイベントをサポートします。次を実行します。

pnpm exec wrangler dev

ローカル handler へ生メールを送ります。

curl -X POST \
  'http://localhost:8787/cdn-cgi/handler/email?from=sender@example.net&to=support@example.com' \
  -H 'Content-Type: text/plain' \
  --data-binary $'From: sender@example.net\nTo: support@example.com\nSubject: Local test\n\nHello from local development.'

ローカルの送信 binding は text と HTML を確認できるファイルとして保存するため、template 検証に適しています。binary 添付のローカル simulation には個別の制限があるため、添付フローは remote test も追加してください。

少なくとも次をテストします。

  • 通常の text と HTML。
  • 非 ASCII の件名と送信者表示名。
  • 重複リクエストと idempotency。
  • cooldown 完了前の認証コード再送。
  • 大きな本文と添付の境界。
  • Header newline injection。
  • 未検証の送信ドメイン、または制限された受信者。
  • bounce、suppression、枠、頻度の error。
  • 一致しない受信アドレス、Catch-all、拒否経路。
  • HTML メール内の script、remote resource、危険な添付ファイル名。

適する用途と適さない用途

適する用途:

  • 登録認証、magic link、パスワードリセット。
  • 注文、請求、デプロイ、監視通知。
  • 個人ドメインのメール転送。
  • contact form とサポートチケット。
  • メールを trigger にする Agent や自動化。
  • SMTP サーバーの保守を減らしたい Cloudflare アプリケーション。

直接使うべきでない用途:

  • 同意のない marketing 一括送信。
  • unsubscribe、complaint、suppression 管理のない Newsletter。
  • 匿名で公開する「代わりにメールを送る」endpoint。
  • IMAP folder、calendar、contact、複数人の shared mailbox が必要な企業メール。
  • 任意の受信本文を、高権限ツールを持つ Agent へ直接渡すこと。

Cloudflare はメール入口と配信インフラを保守しますが、送信者 reputation、ユーザー同意、コンテンツ安全性、頻度制御、データ最小化はアプリケーション自身の責任です。

最終的な判断

Cloudflare Email Service の最も価値ある点は「新しい SMTP provider」になったことではなく、メールを Workers、D1、R2、Queue、Agents と同じランタイム境界へ入れたことです。

受信メールは人のメールボックスへ転送するだけではなく、制約されたイベント入口になります。送信メールも、認証コードのためだけに外部 SDK と別の credential system を導入する必要がなくなります。

プロジェクトがすでに Workers 上にあるなら、次の最小経路から始めます。

  1. Email Routing で support@ を受信する。
  2. Worker binding で認証コードとパスワードリセットを送信する。
  3. D1 で cooldown、一度限りの認証、最小限の log を実装する。
  4. sender allowlist で binding 権限を制限する。
  5. API の成功 response だけでなく、Email Sending log で配信を確認する。

外部 platform から本当に送信する必要が出たときに REST API を追加します。既存システムが SMTP client へ深く依存している場合だけ認証 SMTP を使います。

参考資料

記事はここまで
READER CHANNEL

コメント

00
まだコメントはありません。最初の返信を残してみませんか。