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

Cloudflare SMTP でメール送信:ダッシュボード設定から Nodemailer の本番運用まで

Cloudflare Email Service の SMTP を使うための実践ガイド。ドメイン、API Token、cURL、Nodemailer、Python、制限、応答コード、再試行、安全対策を解説します。

#Cloudflare#SMTP#Email#Nodemailer
暗号化された SMTP 接続からグローバル配信ネットワークへ入るトランザクションメール
既存アプリを Cloudflare Email Service へ接続するために送信層を作り直す必要はありません。標準 SMTP が互換性のある経路になります

Cloudflare Email Service は 2026 年に認証付き SMTP を追加しました。すでに Nodemailer、Python の smtplib、PHPMailer、または SMTP 設定しか持たない製品を使っている場合、送信層全体を作り直さずに配信サービスを切り替えられます。

この記事は独立した SMTP チュートリアルです。Email Routing、受信処理、Email Workers の全体像を繰り返すのではなく、次の一つを完成させます。

smtp.mx.cloudflare.net:465 に接続し、Cloudflare API Token で SMTPS 認証を行い、自分のドメインからトランザクションメールを送信する。

内容は 2026 年 7 月 31 日時点で確認できた Cloudflare Email Service Beta に基づきます。Beta の割り当てや画面は変わる可能性があるため、本番導入前に公式の送信ガイドSMTP API リファレンスを確認してください。

1 分で分かる接続設定

項目
SMTP ホストsmtp.mx.cloudflare.net
ポート465
暗号化暗黙的 TLS、つまり SMTPS
ユーザー名固定文字列 api_token
パスワードEmail Sending: Edit 権限を持つ Cloudflare API Token
認証方式AUTH PLAIN または AUTH LOGIN
FromEmail Sending で有効化したドメインのアドレス

混同しやすい点が三つあります。

  1. ユーザー名は Cloudflare のメールアドレスや Account ID ではなく、常に api_token です。
  2. パスワードは Cloudflare へのログインパスワードではなく、専用 API Token です。
  3. 465 番ポートは接続開始時から TLS を使います。587 番ポートの STARTTLS 設定とは交換できません。

Cloudflare は現在、587 STARTTLS や 25 番ポートの送信リレーを提供していません。25 番ポートは受信用 Email Routing のためのものです。現在の境界は公式 SMTP リファレンスで確認できます。

事前準備

1. 送信ドメインを Cloudflare DNS に置く

Email Sending には Cloudflare DNS が必要です。ダッシュボードで次を開きます。

Cloudflare Dashboard
→ Compute
→ Email Service
→ Email Sending

ドメインを選択してオンボーディングを完了します。Cloudflare は送信者認証、DKIM、バウンス処理に必要な DNS レコードを設定します。ドメインが「設定済み」になってから SMTP を調査してください。ログインに成功しても、送信ドメインが許可されていなければ拒否されます。

2. 最小権限の API Token を作る

ドメインの「接続」ページで SMTP を選択します。ダッシュボードには Email Sending: Edit 権限を持つ API Token の作成ボタンがあります。

ホスト、ポート、ユーザー名、cURL の例を表示する Cloudflare Email Service の SMTP 接続ページ

2026 年 7 月 31 日に取得した画面です。Cloudflare の公開設定値と環境変数のプレースホルダーのみが含まれ、実際の Token は作成、表示、掲載していません。

アカウント所有の Token を優先し、可能な限り範囲を絞ります。

  • メールを送る Cloudflare アカウントだけを許可する。
  • Email Sending: Edit だけを付与する。
  • 適切な有効期限を設定する。
  • デプロイ先の送信元 IP が固定なら、クライアント IP 制限も追加する。
  • アプリごとに Token を分け、個別に監査・失効できるようにする。

完全な Token は作成時に一度だけ表示されます。サーバー側の Secret または実行時環境変数へ保存してください。次の場所には置けません。

  • Git、Markdown、ビルドログ
  • NUXT_PUBLIC_*
  • ブラウザー JavaScript
  • エラー時に出力する SMTP 設定オブジェクト
  • 複数の無関係なアプリで共有する長期 Token

まず cURL で経路を検証する

最小の cURL テストを先に行うと、Cloudflare 側の設定エラーとフレームワーク統合エラーを分離できます。

mail.txt を作成します。

From: Jackie Moon <noreply@sparkles-editor.com>
To: recipient@example.com
Subject: Cloudflare SMTP test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8

Hello,

This message was delivered through Cloudflare Email Service SMTP.

Token の値をシェル履歴に残さず、現在のプロセスへ読み込みます。

read -s CF_EMAIL_SMTP_TOKEN
export CF_EMAIL_SMTP_TOKEN

送信します。

curl --url "smtps://smtp.mx.cloudflare.net:465" \
  --ssl-reqd \
  --user "api_token:${CF_EMAIL_SMTP_TOKEN}" \
  --mail-from "noreply@sparkles-editor.com" \
  --mail-rcpt "recipient@example.com" \
  --upload-file mail.txt

成功すると SMTP は 250 を返します。応答に含まれる Message-ID を保存すると、Email Service ダッシュボードの送信ログと関連付けられます。Cloudflare の公式 cURL 例とも比較してください。

ここで失敗した場合、まだ Nodemailer を変更しないでください。

  • 535:Token が無効、期限切れ、または Email Sending: Edit がない。
  • 550:送信アドレスまたはドメインが許可されていない。
  • 552:メッセージが大きすぎる。
  • 421 / 451:一時的な障害またはレート制限。時間を置いて再試行する。

Node.js から Nodemailer を使う

この構成は Node.js、Docker、VPS、一般的な Serverless Function、または Node 互換環境で動く Nitro サーバー向けです。

依存関係を追加します。

pnpm add nodemailer
pnpm add -D @types/nodemailer

サーバー側の環境変数を定義します。

CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret
SMTP_FROM="Jackie Moon <noreply@sparkles-editor.com>"

Transport を作成します。

import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
  host: 'smtp.mx.cloudflare.net',
  port: 465,
  secure: true,
  auth: {
    user: 'api_token',
    pass: process.env.CF_EMAIL_SMTP_TOKEN,
  },
  connectionTimeout: 10_000,
  greetingTimeout: 10_000,
  socketTimeout: 30_000,
})

const info = await transporter.sendMail({
  from: process.env.SMTP_FROM,
  to: 'recipient@example.com',
  subject: 'Welcome to Signal Log',
  text: 'Your account is ready.',
  html: '<p>Your account is ready.</p>',
})

console.info('Email accepted', {
  messageId: info.messageId,
  accepted: info.accepted.length,
  rejected: info.rejected.length,
})

パスワードを環境変数から読み込む Cloudflare ダッシュボードの Nodemailer 例

Cloudflare の画面例は process.env.CLOUDFLARE_API_TOKEN を使っています。プロジェクトではより具体的な変数名にできますが、値は必ずサーバー側に置きます。

本番コードには envelope の境界も必要です。

const allowedFrom = new Set([
  'noreply@sparkles-editor.com',
  'support@sparkles-editor.com',
])

function assertEmailEnvelope(from: string, to: string) {
  if (!allowedFrom.has(from)) {
    throw new Error('Sender is not allowed')
  }

  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) {
    throw new Error('Recipient is invalid')
  }
}

任意の fromtosubject、HTML を受け取る公開 API は作らないでください。オープンリレーやフィッシング送信に悪用されます。安全な API は userIdtemplateId のような業務パラメータだけを受け取り、サーバーが検証済みの宛先を取得し、許可済みテンプレートから内容を生成します。

Nuxt が Cloudflare Workers 上で動く場合

実行環境を区別する必要があります。

Nodemailer は通常の Node SMTP Socket を対象にしたライブラリです。すでに Cloudflare Workers 上で動く Nuxt アプリは、通常 send_email binding を使う方が自然です。

interface Env {
  EMAIL: SendEmail
}

await env.EMAIL.send({
  from: 'noreply@sparkles-editor.com',
  to: 'recipient@example.com',
  subject: 'Verification code',
  text: 'Your code is 123456',
})

Worker に SMTP API Token を保存せずに済み、binding 自体が権限境界になります。

選択基準は次のとおりです。

状況推奨する入口
Nuxt / API が Cloudflare Workers 上にあるWorkers の send_email binding
外部サービスが HTTPS と Token 管理に適しているREST API
既存の Node、Python、PHP アプリが SMTP に対応済み認証付き SMTP
プリンター、旧 CMS、外部製品が SMTP 設定しか持たない465 SMTPS 対応を確認して SMTP
ブラウザーから直接送る禁止。管理されたサーバー API を使う

三つのサーバー側入口は Cloudflare の配信、ログ、ドメイン認証へ合流します。SMTP の価値は互換性であり、すべての新規サービスを Socket に戻すことではありません。

Python の最小例

Python の標準ライブラリだけで送信できます。

import os
import smtplib
from email.message import EmailMessage

token = os.environ["CF_EMAIL_SMTP_TOKEN"]

message = EmailMessage()
message["From"] = "Jackie Moon <noreply@sparkles-editor.com>"
message["To"] = "recipient@example.com"
message["Subject"] = "Cloudflare SMTP test"
message.set_content("This email was sent over implicit TLS.")

with smtplib.SMTP_SSL("smtp.mx.cloudflare.net", 465, timeout=30) as smtp:
    smtp.login("api_token", token)
    smtp.send_message(message)

重要なのは SMTP_SSL であり、平文接続の後に starttls() を呼ぶ構成ではありません。

制限と添付ファイル

現在のEmail Service プラットフォーム制限は次のとおりです。

  • 1 通あたりの宛先合計は最大 50。
  • SMTP セッションで受け付ける RCPT TO は最大 50 回。
  • 通常のメッセージ総容量は最大 5 MiB。
  • 検証済み宛先へのメッセージは最大 25 MiB。
  • SMTP AUTH のタイムアウトは 30 秒。
  • SMTP DATA のタイムアウトは 300 秒。
  • カスタムヘッダー合計は 16 KiB。
  • Subject は 998 文字まで。

総容量には MIME エンコード後の本文と添付ファイルが含まれます。Base64 ではバイナリが約 3 分の 1 増えることが多いため、4.9 MiB のファイルが 5 MiB のメールに収まるとは限りません。

ダッシュボードの日次割り当ては、アカウント、Beta 段階、利用状況によって変わる可能性があります。一度見た値をハードコードせず、現在の画面を確認して送信キューに余裕を残します。

エラー処理と再試行

SMTP 応答は再試行すべきかを表しています。

応答意味対応
250Cloudflare が受理したMessage-ID を保存し、重複送信しない
421 / 451一時障害または制限ジッター付き指数バックオフ
535認証失敗自動再試行を止め、Token を確認
550送信者、ドメイン、宛先ポリシーによる拒否設定またはデータを直してから送る
552メッセージが大きすぎる本文または添付を減らす

再試行は 30 秒、2 分、10 分、30 分のようにし、最大回数を固定します。業務上の送信には冪等キーが必要です。

password-reset:{userId}:{resetRequestId}
invoice:{invoiceId}:issued

一時エラーはキューへ、恒久エラーは失敗または確認状態へ送ります。すべての非 250 を即時再試行したり、ネットワークの揺れで同じ認証コードを十回送ったりしてはいけません。

250 は最終到達ではない

250 は Cloudflare が受理したという意味で、最終受信箱への到着を保証しません。本番環境では次を監視します。

  • Delivery、Bounce、Deferred、Suppression
  • Gmail、Outlook、QQ メールなど主要宛先の結果
  • From、Return-Path、DKIM、DMARC の整合
  • ハードバウンスした宛先の即時停止
  • 苦情率と無効アドレスの増加
  • 認証コードとパスワード再設定メールの明確な有効期限

トランザクションメールには安定して識別できる送信者を使い、テキスト版と HTML 版を用意します。Reply-To を偽装せず、認証用経路で広告を大量送信せず、ユーザー入力をメールヘッダーへ直接入れないでください。

本番チェックリスト

  • 送信ドメインが Email Sending で設定済み
  • 465 番ポートと暗黙的 TLS を使用
  • ユーザー名が正確に api_token
  • Token の権限が Email Sending: Edit のみ
  • Token がサーバー Secret にあり、Git、ログ、フロントにない
  • From はサーバー側 allowlist から選ぶ
  • 宛先を検証、重複排除、長さ制限している
  • HTML は固定テンプレートで、テキスト版もある
  • 421 / 451 はジッター付き指数バックオフ
  • 535 / 550 / 552 を無条件再試行しない
  • Message-ID、テンプレート名、冪等キーを記録し、Token は記録しない
  • バウンス、抑制、苦情、日次割り当てを監視

最後の選択

安定稼働中の Node、Python、PHP アプリでは、Cloudflare SMTP の利点は明快です。ホスト、ポート、認証情報を変更するだけで Cloudflare の配信とログへ接続できます。

Workers ネイティブのアプリなら、従来サーバーを再現する必要はありません。Email binding を優先してください。SMTP は既存エコシステムへの橋であり、唯一の入口ではありません。

参考資料

記事はここまで
READER CHANNEL

コメント

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