Back to article index
TRANSMISSION / CYAN17 min read

Sending Email with Cloudflare SMTP—from Dashboard Setup to Nodemailer

A practical Cloudflare Email Service SMTP guide covering domain onboarding, API Tokens, cURL, Nodemailer, Python, limits, response codes, retries, and production security.

#Cloudflare#SMTP#Email#Nodemailer
A transactional email entering a global delivery network through an encrypted SMTP connection
Connecting an existing application to Cloudflare Email Service does not require rewriting its sending layer; standard SMTP is the compatibility path

Cloudflare Email Service added authenticated SMTP in 2026. If an application already uses Nodemailer, Python smtplib, PHPMailer, or a product that only exposes SMTP settings, you can change the delivery provider without rebuilding the whole sending layer.

This is a standalone SMTP tutorial. It does not repeat the broader topics of Email Routing, inbound processing, or Email Workers. It has one concrete goal:

Connect to smtp.mx.cloudflare.net:465, authenticate with a Cloudflare API Token over SMTPS, and send a transactional email from your own domain.

This article reflects the Cloudflare Email Service Beta visible on July 31, 2026. Beta quotas and dashboard screens can change, so recheck the official sending guide and SMTP API reference before production rollout.

The one-minute configuration

SettingValue
SMTP hostsmtp.mx.cloudflare.net
Port465
EncryptionImplicit TLS, also known as SMTPS
UsernameThe literal string api_token
PasswordA Cloudflare API Token with Email Sending: Edit
AuthenticationAUTH PLAIN or AUTH LOGIN
From addressAn address on a domain enabled in Email Sending

Three values are easy to confuse:

  1. The username is not your Cloudflare email or Account ID. It is always api_token.
  2. The password is not your Cloudflare login password. It is a dedicated API Token.
  3. Port 465 starts TLS immediately. A port 587 STARTTLS configuration is not interchangeable with it.

Cloudflare currently does not provide port 587 STARTTLS or outbound relay on port 25. Port 25 is reserved for inbound Email Routing. See the official SMTP reference for the current protocol boundary.

Prerequisites

1. Put the sending domain on Cloudflare DNS

Email Sending requires Cloudflare DNS. In the dashboard, open:

Cloudflare Dashboard
→ Compute
→ Email Service
→ Email Sending

Select the domain and finish onboarding. Cloudflare configures the DNS records used for sender authorization, DKIM, and bounce handling. Wait until the domain is marked configured before debugging SMTP; a valid login can still be rejected if the sender domain is not authorized.

2. Create a least-privilege API Token

Open the domain's Connect page and select SMTP. The dashboard offers to create an API Token with Email Sending: Edit.

Cloudflare Email Service SMTP connection page showing the host, port, username, and cURL sample

Captured on July 31, 2026. The image contains only Cloudflare configuration values and an environment-variable placeholder. No real token was created, displayed, or included in this article.

Prefer an account-owned token, then narrow it as much as possible:

  • Grant access only to the account that sends email.
  • Grant only Email Sending: Edit.
  • Set a sensible expiration.
  • Add client IP filters when the deployment has stable egress addresses.
  • Give every application its own token so it can be audited and revoked independently.

The complete token is shown only once. Store it in server-side secrets or runtime environment variables. Never:

  • Commit it to Git, Markdown, or build logs.
  • Put it in a NUXT_PUBLIC_* variable.
  • Expose it to browser JavaScript.
  • Log the SMTP transport configuration on errors.
  • Share one long-lived token across unrelated applications.

Verify the path with cURL first

A minimal cURL test separates Cloudflare configuration errors from framework integration errors.

Create 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.

Read the token into the current shell without placing the value in command history:

read -s CF_EMAIL_SMTP_TOKEN
export CF_EMAIL_SMTP_TOKEN

Send the message:

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

A successful SMTP transaction returns 250. Preserve the Message-ID from the response so it can be correlated with Email Service dashboard logs. Compare the command with Cloudflare's official cURL example.

If this step fails, do not start changing Nodemailer yet:

  • 535: the token is invalid, expired, or lacks Email Sending: Edit.
  • 550: the sender address or domain is not allowed.
  • 552: the message is too large.
  • 421 / 451: a temporary failure or rate limit; retry later.

Use Nodemailer from Node.js

This setup is for Node.js, Docker, a VPS, conventional serverless functions, or a Nitro server running in a Node-compatible environment.

Install the packages:

pnpm add nodemailer
pnpm add -D @types/nodemailer

Define server-side variables:

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

Create the 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,
})

Nodemailer sample in the Cloudflare dashboard, with the password read from an environment variable

Cloudflare's dashboard sample uses process.env.CLOUDFLARE_API_TOKEN. A project may use a more specific name, but the value must remain server-side.

Production code also needs an envelope boundary:

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')
  }
}

Do not expose a public endpoint that accepts arbitrary from, to, subject, and HTML values. It can become an open relay or a phishing tool. A safer endpoint accepts business inputs such as userId and templateId; the server then loads a verified recipient and renders a whitelisted template.

What if Nuxt runs on Cloudflare Workers?

The runtime distinction matters.

Nodemailer targets conventional Node SMTP sockets. A Nuxt application already running on Cloudflare Workers should normally use the 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',
})

This avoids storing an SMTP API Token in the Worker, and the binding carries its own permission boundary.

Use this decision table:

SituationRecommended entry point
Nuxt or an API is deployed on Cloudflare WorkersWorkers send_email binding
An external service is comfortable with HTTPS and token managementREST API
An existing Node, Python, or PHP application already supports SMTPAuthenticated SMTP
A printer, legacy CMS, or third-party product only accepts SMTP settingsSMTP, after confirming port 465 SMTPS support
Browser-side sendingNever; use a controlled server endpoint

All three server-side entry points feed Cloudflare's delivery pipeline, logs, and domain authentication. SMTP is about compatibility, not a requirement to move every new service back to sockets.

Minimal Python example

Python's standard library is sufficient:

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)

The important part is SMTP_SSL, not a plaintext connection followed by starttls().

Limits and attachments

According to the current Email Service platform limits:

  • One email can have at most 50 combined recipients.
  • An SMTP session accepts at most 50 RCPT TO commands.
  • A normal message is limited to 5 MiB.
  • A message to a verified destination can be up to 25 MiB.
  • SMTP AUTH times out after 30 seconds.
  • SMTP DATA times out after 300 seconds.
  • Custom headers are limited to 16 KiB.
  • A Subject is limited to 998 characters.

The message size includes the MIME-encoded body and attachments. Base64 commonly adds roughly one third to a binary attachment, so a 4.9 MiB file does not necessarily fit in a 5 MiB email.

Daily quota shown in the dashboard can vary by account, Beta stage, and usage. Do not hard-code a number observed once. Check the current dashboard and reserve capacity in your own queue.

Error handling and retries

SMTP response classes already indicate whether a retry makes sense:

ResponseMeaningAction
250Cloudflare accepted the messageStore the Message-ID and do not send it again
421 / 451Temporary failure or throttlingExponential backoff with jitter
535Authentication failedStop automatic retries and inspect the token
550Sender, domain, or recipient policy rejected the messageFix configuration or data first
552Message too largeReduce the body or attachments

A reasonable retry schedule is 30 seconds, 2 minutes, 10 minutes, and 30 minutes, with a fixed maximum. Every business send needs an idempotency key:

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

Queue temporary failures and route permanent failures to a failed or review state. Do not retry every non-250 response immediately, and do not send the same verification code ten times because the network was unstable.

Delivery does not end at 250

250 means Cloudflare accepted the message, not that it reached the final inbox. Production monitoring should include:

  • Delivery, Bounce, Deferred, and Suppression results.
  • Gmail, Outlook, QQ Mail, and other major destination outcomes.
  • Alignment among From, Return-Path, DKIM, and DMARC.
  • Immediate suppression of hard-bounce addresses.
  • Complaint rate and invalid-address growth.
  • Explicit expiry for verification and password-reset messages.

Transactional email should use a stable, recognizable sender and provide both plain text and HTML. Do not spoof Reply-To, use an authentication channel for marketing blasts, or place raw user input in email headers.

Production checklist

  • The sender domain is configured in Email Sending.
  • The client uses port 465 with implicit TLS.
  • The username is exactly api_token.
  • The token has only Email Sending: Edit.
  • The token remains in server secrets, never Git, logs, or frontend code.
  • From addresses come from a server-side allowlist.
  • Recipients are validated, deduplicated, and length-limited.
  • HTML comes from fixed templates and includes a text alternative.
  • 421 / 451 use jittered exponential backoff.
  • 535 / 550 / 552 are not retried blindly.
  • Logs store Message-ID, template name, and idempotency key—not the token.
  • Bounces, suppressions, complaints, and daily quota are monitored.

The final choice

For an established Node, Python, or PHP application, Cloudflare SMTP has a straightforward advantage: change the host, port, and credentials, then join Cloudflare's delivery and logging system.

For an application native to Workers, there is no need to emulate a traditional server. Prefer the Email binding. SMTP is a bridge to the existing ecosystem, not the only entrance.

References

End of signal
READER CHANNEL

Comments

00
No comments yet. Be the first to respond.