Feature request: built-in SMTP email transport in Emdash core
Summary
Provide a first-class SMTP email transport in Emdash core so operators can
send mail through any standard SMTP server (host / port / username / password)
instead of only through HTTP transactional APIs. A plugin cannot do this, by
design — only the host runtime can open the TCP socket SMTP requires.
Motivation
Today, sending email from Emdash on Cloudflare Workers means using a provider
with an HTTP API (e.g. the emdash-plugin-brevo plugin, which calls
POST https://api.brevo.com/v3/smtp/email). That works well, but a large class
of operators only have generic SMTP credentials:
- Brevo's own SMTP relay (
smtp-relay.brevo.com:587, authenticated with an
xsmtpsib- SMTP key) — distinct from its HTTP API key (xkeysib-).
- Office365 / Google Workspace / Fastmail / Amazon SES SMTP endpoints.
- Self-hosted Postfix and corporate mail relays.
None of these can be reached from a plugin.
Why this can't be a plugin
Emdash sandboxed plugins are given exactly one network primitive —
ctx.http.fetch() — which speaks HTTP/HTTPS only and is gated by the
plugin's allowedHosts allowlist. There is no TCP/socket capability in the
plugin capability vocabulary. SMTP is a raw TCP protocol (EHLO / STARTTLS /
AUTH / MAIL FROM / RCPT TO / DATA), so a plugin physically cannot open an SMTP
connection.
Cloudflare Workers can open TCP sockets via connect() from
cloudflare:sockets, but only host code can — that module is not exposed to
the sandbox. Therefore an SMTP transport has to live in core (or in a
host-trusted/native extension point), not in a sandboxed plugin.
Proposal
Add an optional core email transport, smtp, that registers as the email
delivery provider so existing ctx.email.send() callers and the
email:deliver hook contract keep working unchanged.
Configuration
Via env/wrangler vars (secrets) and/or the admin Email settings page:
| Key |
Example |
Notes |
EMAIL_SMTP_HOST |
smtp-relay.brevo.com |
|
EMAIL_SMTP_PORT |
587 |
587 (STARTTLS) or 465 (implicit TLS) |
EMAIL_SMTP_SECURE |
starttls | tls |
|
EMAIL_SMTP_USER |
you@example.com |
|
EMAIL_SMTP_PASS |
xsmtpsib-… |
stored as a secret |
EMAIL_SMTP_FROM |
Site <noreply@example.com> |
default sender |
Implementation sketch
- Use
connect(\${host}:${port}`, { secureTransport: port === 465 ? "on" : "starttls", allowHalfOpen: false })fromcloudflare:sockets`.
- Minimal SMTP client: read greeting →
EHLO → (STARTTLS + socket.startTls() when secure=starttls) → AUTH LOGIN/PLAIN → MAIL FROM → RCPT TO → DATA → dot-stuffed MIME body → QUIT.
- Build a compliant MIME message (headers,
text/plain + text/html multipart/alternative, quoted-printable/base64, message-id, date).
- Enforce a connect/overall timeout; surface SMTP reply codes as structured errors.
Caveats to call out in the implementation
- Cloudflare blocks outbound port 25; this transport targets submission
ports 587/465 only, and should validate/refuse 25 with a clear message.
- TLS must be required (STARTTLS upgrade verified, or implicit TLS on 465);
never fall back to plaintext auth on an unencrypted channel.
- Some hosts (corporate relays) may not be reachable from Cloudflare egress IPs;
document this alongside the existing dynamic-egress-IP guidance.
Alternatives considered
- HTTP→SMTP gateway service — works from a plugin (it's just HTTP), but adds
a third party in the mail path and means handing SMTP credentials to that
service. Rejected as a default.
- Per-plugin SMTP — impossible (no TCP capability in the sandbox), which is
the reason for this request.
- Status quo (HTTP transactional APIs only) — fine for Brevo/SendGrid/etc.
API users, but excludes everyone who only has SMTP credentials.
Backward compatibility
Additive. Existing HTTP-API providers (including the Brevo plugin) continue to
work; smtp is just another selectable transport. If both a core SMTP transport
and an exclusive email:deliver plugin are configured, document the precedence
(suggest: an explicitly-configured plugin transport wins, else core SMTP).
Acceptance criteria
Feature request: built-in SMTP email transport in Emdash core
Summary
Provide a first-class SMTP email transport in Emdash core so operators can
send mail through any standard SMTP server (host / port / username / password)
instead of only through HTTP transactional APIs. A plugin cannot do this, by
design — only the host runtime can open the TCP socket SMTP requires.
Motivation
Today, sending email from Emdash on Cloudflare Workers means using a provider
with an HTTP API (e.g. the
emdash-plugin-brevoplugin, which callsPOST https://api.brevo.com/v3/smtp/email). That works well, but a large classof operators only have generic SMTP credentials:
smtp-relay.brevo.com:587, authenticated with anxsmtpsib-SMTP key) — distinct from its HTTP API key (xkeysib-).None of these can be reached from a plugin.
Why this can't be a plugin
Emdash sandboxed plugins are given exactly one network primitive —
ctx.http.fetch()— which speaks HTTP/HTTPS only and is gated by theplugin's
allowedHostsallowlist. There is no TCP/socket capability in theplugin capability vocabulary. SMTP is a raw TCP protocol (EHLO / STARTTLS /
AUTH / MAIL FROM / RCPT TO / DATA), so a plugin physically cannot open an SMTP
connection.
Cloudflare Workers can open TCP sockets via
connect()fromcloudflare:sockets, but only host code can — that module is not exposed tothe sandbox. Therefore an SMTP transport has to live in core (or in a
host-trusted/native extension point), not in a sandboxed plugin.
Proposal
Add an optional core email transport,
smtp, that registers as the emaildelivery provider so existing
ctx.email.send()callers and theemail:deliverhook contract keep working unchanged.Configuration
Via env/wrangler vars (secrets) and/or the admin Email settings page:
EMAIL_SMTP_HOSTsmtp-relay.brevo.comEMAIL_SMTP_PORT587EMAIL_SMTP_SECUREstarttls|tlsEMAIL_SMTP_USERyou@example.comEMAIL_SMTP_PASSxsmtpsib-…EMAIL_SMTP_FROMSite <noreply@example.com>Implementation sketch
connect(\${host}:${port}`, { secureTransport: port === 465 ? "on" : "starttls", allowHalfOpen: false })fromcloudflare:sockets`.EHLO→ (STARTTLS+socket.startTls()whensecure=starttls) →AUTH LOGIN/PLAIN→MAIL FROM→RCPT TO→DATA→ dot-stuffed MIME body →QUIT.text/plain+text/htmlmultipart/alternative, quoted-printable/base64, message-id, date).Caveats to call out in the implementation
ports 587/465 only, and should validate/refuse 25 with a clear message.
never fall back to plaintext auth on an unencrypted channel.
document this alongside the existing dynamic-egress-IP guidance.
Alternatives considered
a third party in the mail path and means handing SMTP credentials to that
service. Rejected as a default.
the reason for this request.
API users, but excludes everyone who only has SMTP credentials.
Backward compatibility
Additive. Existing HTTP-API providers (including the Brevo plugin) continue to
work;
smtpis just another selectable transport. If both a core SMTP transportand an exclusive
email:deliverplugin are configured, document the precedence(suggest: an explicitly-configured plugin transport wins, else core SMTP).
Acceptance criteria
ctx.email.send()delivers via SMTP when thesmtptransport is configured.