在 Cloudflare Workers 里发事务邮件(2026):用 HTTP 调 API
在 Cloudflare Worker 里用 fetch 调 Rovela POST /emails:wrangler secrets 存放 sending_access 的 ROVELA_API_KEY、Idempotency-Key、已验证域名——OTP、魔法链接、收据。不是 Cloudflare Email Workers。
Cloudflare Workers + Rovela(不是 Cloudflare Email Workers)
本文讲的是:在 Cloudflare Worker 里用 fetch 调用 Rovela 的 HTTP API,发送事务邮件——OTP 验证码、魔法登录链接、收据、密码重置等。
它不是 Cloudflare Email Workers、Email Routing,也不是 Cloudflare 自带的「发信」产品。那些是 Cloudflare 侧用于在你域名上接收或路由邮件的能力。这里 Worker 只是一个 HTTPS 客户端:把 sending_access 密钥放在 Worker secrets 里,向 https://api.rovela.dev/emails POST JSON,由 Rovela 入队、DKIM 签名并投递。
若你要的是面向 AI agent 的脚手架(Cursor / Claude 写同一条 curl),见 AI agent 用 HTTP 发邮件。各框架的 OTP / 魔法链接教程在别处(Next.js、Express OTP、Go OTP 等)。本文只盯 Worker 边缘:密钥、fetch、幂等与已验证域名。
你负责什么,Rovela 发什么
Worker(或其背后的源站)负责:
- 生成 OTP / 魔法链接 token / 收据内容
- 存储哈希、过期与核销逻辑(Workers KV、D1、Durable Objects 或源站数据库)
- 边缘限流与防滥用
- 为每次逻辑发送选择稳定的
Idempotency-Key
Rovela 负责:
POST https://api.rovela.dev/emails,Authorization: Bearer+sending_access密钥- 入队、签名、重试、退信/投诉处理
返回 200 表示已接受并入队,不等于「已进收件箱」(Send Email API、Quick Start)。
前置条件
- 在 Rovela 控制台验证发信域名。未验证域名的
from会直接400拒绝——不会入队后静默丢掉(Quick Start)。 - 形如
re_…的 API 密钥。生产 Worker 优先用sending_access:只能发信,不能做别的(Authentication)。 - 用
wrangler管理的 Worker 项目:密钥永不进源码或公开绑定。
不要把密钥放进前端、公开仓库,或会打进脚本的普通 vars。用加密 secrets。
用 Wrangler secrets 存密钥
# 在 Worker 项目目录
npx wrangler secret put ROVELA_API_KEY
# 按提示粘贴 re_… 的 sending_access 密钥
# 可选:发件人地址用普通 var(若不敏感)
npx wrangler vars put ROVELA_FROM
# 例如 Acme <security@mail.example.com>
在 wrangler.toml / wrangler.jsonc 里只声明绑定名,不写密钥值:
name = "acme-mail"
main = "src/index.ts"
compatibility_date = "2026-11-01"
[vars]
ROVELA_FROM = "Acme <security@mail.example.com>"
# ROVELA_API_KEY 来自 `wrangler secret put` —— 不要提交
TypeScript 里声明环境:
export interface Env {
ROVELA_API_KEY: string;
ROVELA_FROM: string;
}
最小 Worker:fetch → POST /emails
// src/index.ts
export interface Env {
ROVELA_API_KEY: string;
ROVELA_FROM: string;
}
type SendBody = {
to: string;
subject: string;
html: string;
text: string;
idempotencyKey: string;
};
async function sendTransactional(env: Env, msg: SendBody) {
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${env.ROVELA_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
"Idempotency-Key": msg.idempotencyKey.slice(0, 256),
},
body: JSON.stringify({
from: env.ROVELA_FROM,
to: msg.to,
subject: msg.subject,
html: msg.html,
text: msg.text,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
// 不要打印 API 密钥。记 status 与邮件 id 即可。
throw new Error(`rovela_send_failed:${res.status}`);
}
return data as { object: string; id: string };
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("method_not_allowed", { status: 405 });
}
// 示例:应用在 mint OTP 之后调用的边缘接口
const body = (await request.json()) as {
to?: string;
code?: string;
sendId?: string;
};
const to = (body.to || "").trim().toLowerCase();
const code = (body.code || "").trim();
const sendId = (body.sendId || "").trim();
if (!to || !/^\d{6}$/.test(code) || !sendId) {
return Response.json({ error: "invalid_request" }, { status: 400 });
}
// 同一逻辑发送保持稳定 —— Worker/平台重试时复用
const idempotencyKey = `otp-${sendId}`;
try {
const queued = await sendTransactional(env, {
to,
subject: `Your Acme code: ${code}`,
html: `<p>Your verification code is</p><p style="font-size:28px;font-weight:600;letter-spacing:4px"><b>${code}</b></p><p>It expires in 10 minutes. If you did not ask for it, ignore this email.</p>`,
text: `Your verification code is ${code}. It expires in 10 minutes. If you did not ask for it, ignore this email.`,
idempotencyKey,
});
return Response.json({ ok: true, emailId: queued.id });
} catch {
return Response.json({ error: "send_failed" }, { status: 502 });
}
},
};
纯 JavaScript 形态相同——fetch、Bearer 头、JSON 体。不需要安装 Rovela 的 Cloudflare SDK;Workers 本身就能发 HTTPS。
密钥配好后再用 curl 打你的 Worker URL 做冒烟。DNS/密钥排查可先直连 Rovela 一次:
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer $ROVELA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: worker-smoke-2026-11-05" \
-d '{
"from": "Acme <security@mail.example.com>",
"to": "you@example.com",
"subject": "Worker smoke test",
"html": "<p>It worked.</p>",
"text": "It worked."
}'
Idempotency-Key:边缘重试不发双信
Cloudflare、你的客户端,以及自己的 waitUntil / 队列消费者都会重试。没有 Idempotency-Key,一次超时可能投递两封不一致的 OTP,或两封魔法链接。
按 emails API:
- Header ≤ 256 字符
- Rovela 记住 24 小时,重放返回原响应
- 从该次逻辑发送的稳定标识派生(
otp-{hashPrefix}、magic-{tokenId}、receipt-{orderId}) - 有意重发并 mint 新码/token 时必须用新 key
headers: {
Authorization: `Bearer ${env.ROVELA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `otp-${codeHash.slice(0, 32)}`,
}
边缘上的 OTP 与魔法链接注意点
Rovela 不会生成 OTP 或登录 token。按 邮箱验证码:用 CSPRNG 生成,只存 HMAC(永不存明文数字),设过期与尝试次数,再同时发 html 与 text。
| 流程 | 边缘关注点 | 正文 |
|---|---|---|
| OTP | 短秘密 → KV/D1/源站里 HMAC + pepper | 主题 + html/text 里放数字 |
| 魔法链接 | 不透明 token;安全域上建议关点击追踪 | html 和 text 里放绝对 HTTPS URL |
| 收据 | 对 orderId / 发票号做幂等 | 订单摘要;附件可后加 |
数字码 vs 链接的产品选择:邮箱验证:验证码 vs 链接。认证邮件进垃圾箱清单:为什么 OTP / 魔法链接进垃圾箱。
尽量让核销逻辑不暴露在公网:Worker 校验来自源站的签名内部请求,或仅在源站写好哈希之后再由 Worker 发送。
限流与 429
发送过快时 Rovela 可能返回 429(API errors)。在 Worker 侧:
- 若不确定第一次是否已被接受,退避后用同一
Idempotency-Key重试 - 调 Rovela 前先做按 IP / 按邮箱闸门,避免滥用烧光配额而不只是边缘 CPU
- 每个收件人计入配额;
cc/bcc同样计数——认证事务邮件通常只要一个to
不要在 Cron Trigger 或队列消费者里对 429 忙等死循环。
已验证域名、最小权限、日志
- 只从已验证域名上的地址发送(Quick Start)。
ROVELA_API_KEY用sending_access;读事件等管理操作用full_access(Authentication)。- 多租户发信时可把密钥限制到单一域名。
- 永远不要
console.logBearer token。记邮件 id、状态码、幂等 key 前缀即可。 - OTP / 魔法链接优先用安全子域,如
security@mail.example.com。
套餐与配额见 Pricing——以该页为准,本文不编造数字。
Workers vs Email Routing(避免混淆)
| 本文(Worker → Rovela HTTP) | Cloudflare Email Routing / Email Workers | |
|---|---|---|
| 方向 | 出站事务发送 | 主要是入站路由 / 处理 |
| 传输 | fetch 到 api.rovela.dev | 你域名上的 Cloudflare 邮件管道 |
| 鉴权 | Rovela sending_access Bearer | Cloudflare zone / Email Workers API |
| 产品邮件 DKIM | Rovela 在你已验证域名上签名 | 不能替代 Rovela 发信路径 |
同一产品可以入站用 Email Routing、出站用 Rovela。查文档或 Stack Overflow 时不要把两者当成一回事。
上线前清单
- 域名已验证;
from匹配该域。 ROVELA_API_KEY用wrangler secret put(仅sending_access)。- Worker 用
fetch+ 认证邮件同时带html和text。 - 每次逻辑发送用稳定
Idempotency-Key;新码/token → 新 key。 - OTP/token 密码学在你自己的存储里——不在 Rovela。
- 处理非 2xx 与
429,且不把密钥打进日志。 - 记住:这是对 Rovela 的 HTTP,不是 Cloudflare Email Workers「发信」。
相关阅读
- 文档:Quick Start · Authentication · POST /emails · 邮箱验证码。
- Agent(同一 HTTP、不同受众):AI agent 用 HTTP 发邮件。
- 认证 UX:邮箱验证:验证码 vs 链接 · OTP / 魔法链接进垃圾箱。
- 框架姊妹篇:Next.js 魔法链接 · Express OTP · Spring Boot OTP · Go OTP。
**一句话:**把 sending_access 密钥放进 Worker secrets,用带 Idempotency-Key 的 fetch 调 POST /emails,OTP/魔法链接状态放在你自己的存储里——Cloudflare Workers 是运行时,Rovela 是邮件管道。这不是 Cloudflare Email Workers。