Files
cloudflare_temp_email/vitepress-docs/docs/zh/guide/config-send-mail.md
jiaxin ebeb94ed23 fix: auto initialize default send balance (#985)
* fix: auto initialize default send balance

* fix: tighten send access auto init flow

* refactor: centralize send balance state

* fix: separate legacy repair from admin control in send balance

Add an `address_sender.source` column to distinguish legacy / auto /
user / admin rows. `ensureDefaultSendBalance` now only repairs rows
with `source IS NULL`, so admin-disabled and user-requested rows are
never overwritten. Admin POST writes tag `source = 'admin'`; new
auto-init inserts tag `'auto'`; `requestSendMailAccess` inserts tag
`'user'`.

Bumps DB_VERSION to v0.0.8 with the usual `PRAGMA table_info` guarded
ALTER, plus a standalone SQL patch under db/.

Adds E2E regressions: legacy repair path, admin-disabled rows stay
disabled across settings and send, send after admin deletion
auto-initializes a fresh row.

* fix: drop runtime legacy repair; backfill source='legacy' on migrate

Pre-v0.0.8 schema cannot distinguish legacy request-send-access
remnants from admin-disabled rows — both share `balance = 0,
enabled = 0`. Letting ensureDefaultSendBalance repair that shape on
upgrade could silently re-enable an admin-disabled row.

Remove the runtime repair path entirely:

- `ensureDefaultSendBalance` now uses `ON CONFLICT(address) DO NOTHING`;
  existing rows are never touched.
- The v0.0.8 migration (and the matching SQL patch) backfills every
  pre-existing row with `source = 'legacy'`, making pre-migration
  state explicitly off-limits to runtime auto-init.
- E2E: flip the legacy test to the negative direction — a
  `source='legacy'` zero-balance row stays untouched by settings
  reads and send attempts. Harden `resetSenderToLegacy` to return
  404 when `meta.changes < 1`.
- Update changelog and docs: legacy/admin-disabled rows must be
  restored manually via the admin UI.

* refactor: collapse send balance auto-init to missing-row insert

Per review feedback: the runtime guarantee we actually need is
"create an address_sender row when one is missing, leave existing
rows alone". Once `ensureDefaultSendBalance` switched to
`ON CONFLICT DO NOTHING`, the `source` column, the v0.0.8 migration,
and the `resetSenderToLegacy` test endpoint became dead weight —
the DO NOTHING path already protects admin-disabled and admin-edited
rows without any provenance metadata.

- Drop `address_sender.source` and the v0.0.8 migration; revert
  DB_VERSION to v0.0.7. No schema change ships with this PR.
- Strip the `source` field from `ensureDefaultSendBalance`,
  `requestSendMailAccess`, and the admin-update path.
- Remove the `/admin/test/reset_sender_to_legacy` test endpoint and
  its E2E helper; the negative legacy-repair test it served is no
  longer needed because the runtime no longer touches existing rows.
- E2E coverage stays focused on the three guardrails: missing-row
  auto-init, admin-disabled rows stay disabled, admin deletion
  triggers a fresh re-insert.
- Tighten changelog and docs to "auto-initialize missing rows".

* docs: align common-issues with missing-row-only auto-init

The FAQ entries for "DEFAULT_SEND_BALANCE set but still No balance"
still described the old behaviour of repairing legacy
`balance = 0 && enabled = 0` rows. Rewrite both zh and en rows to
match the current runtime: only addresses with no existing
`address_sender` row get auto-initialised; legacy, admin-disabled,
and admin-edited rows must be restored manually through the admin
console.
2026-04-20 12:40:14 +08:00

6.3 KiB
Raw Blame History

配置发送邮件

::: tip 推荐方案 推荐使用 Cloudflare send_email binding 作为默认发信通道。绑定 SEND_MAIL 并完成 Email Routing onboarding 后,即可直接向任意外部地址发信。

Workers Paid 每月含 3,000 封,超出部分 $0.35 / 1000 封。 :::

发信通道优先级

每次 /api/send_mail 请求按如下顺序匹配通道,命中即发送

顺序 条件 通道 扣 balance
1 SEND_MAIL 已绑定 收件人在 verifiedAddressList Cloudflare binding兼容模式
2 RESEND_TOKENRESEND_TOKEN_<DOMAIN> 已配置 Resend API
3 SMTP_CONFIG 含当前域名配置 worker-mailer SMTP
4 SEND_MAIL 已绑定(以上均未命中) Cloudflare binding推荐主通道
以上均未命中 抛错

Note

binding 发信失败会直接报错。

使用 Cloudflare send_email binding推荐

仅 CLI 部署时使用,在 wrangler.toml 中添加:

# 通过 Cloudflare send_email binding 发送邮件
send_email = [
   { name = "SEND_MAIL" },
]

[!warning] 重要 绑定名必须为 SEND_MAIL,与 Cloudflare 官方文档示例中的 SEND_EMAIL 不同。

完成下列步骤后即可直接向任意外部地址发信:

  1. 在 Cloudflare Dashboard 给对应域名开启 Email Routing 并完成 onboarding
  2. wrangler.toml 添加上述 send_email 绑定
  3. 部署 Worker

无需配置任何额外的 env var。

使用 Resend 发送邮件

注册 https://resend.com/domains 根据提示添加 DNS 记录,

API KEYS 页面创建 api key

然后执行下面的命令,将 RESEND_TOKEN 添加到 secrets 中

Note

如果你觉得麻烦,也可以直接明文放在 wrangler.toml[vars] 下面,但是不推荐这样做

如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 Variables and Secrets 下面

# 切换到 worker 目录
cd worker
wrangler secret put RESEND_TOKEN

如果你有多个域名,对应不同的 api key,可以在 wrangler.toml 中添加多个 secret, 名称为 RESEND_TOKEN_ + <. 换成 _ 的 大写域名>,例如

wrangler secret put RESEND_TOKEN_XXX_COM
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ

使用 SMTP 发送邮件

SMTP_CONFIG 的格式如下,key 必须是你自己的发信域名value 为 SMTP 配置。

SMTP 配置格式详情可以参考 zou-yu/worker-mailer

[!warning] 重要 JSON 中的 key如下面示例中的 your-domain.com)必须替换为你自己的域名,即 DOMAINS 变量中配置的域名。 这是最常见的配置错误之一,请勿直接复制示例中的域名。

{
    "your-domain.com": {
        "host": "smtp.example.com",
        "port": 465,
        "secure": true,
        "authType": [
            "plain",
            "login"
        ],
        "credentials": {
            "username": "your-smtp-username",
            "password": "your-smtp-password"
        }
    }
}

字段说明:

字段 说明
keyyour-domain.com 你的发信域名,必须与 DOMAINS 中配置的域名一致
host SMTP 服务器地址,如 smtp.mailgun.orgsmtp.gmail.com 或你自建的 SMTP 服务器地址
port SMTP 端口,通常 465SSL587STARTTLS
secure 是否使用 SSL/TLS端口 465 时设为 true,端口 587 时设为 false
authType 认证方式,一般使用 ["plain", "login"]
credentials.username SMTP 服务器的登录用户名
credentials.password SMTP 服务器的登录密码

如果你有多个域名使用不同的 SMTP 服务,在同一个 JSON 中添加多个 key 即可:

{
    "domain-a.com": {
        "host": "smtp.mailgun.org",
        "port": 465,
        "secure": true,
        "authType": ["plain", "login"],
        "credentials": { "username": "user@domain-a.com", "password": "xxx" }
    },
    "domain-b.com": {
        "host": "smtp.gmail.com",
        "port": 465,
        "secure": true,
        "authType": ["plain", "login"],
        "credentials": { "username": "user@gmail.com", "password": "app-password" }
    }
}

然后执行下面的命令,将 SMTP_CONFIG 添加到 secrets 中

Note

如果你觉得麻烦,也可以直接明文放在 wrangler.toml[vars] 下面,但是不推荐这样做

如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 Variables and Secrets 下面

# 切换到 worker 目录
cd worker
wrangler secret put SMTP_CONFIG

发信余额机制

用户发送邮件需要有发信余额。余额机制如下:

  1. 自动初始化默认额度:当 DEFAULT_SEND_BALANCE > 0 时,用户打开前端发信页或第一次调用发信接口时,系统会自动为该地址初始化默认额度
  2. 手动申请:如果 DEFAULT_SEND_BALANCE = 0,用户仍可以在前端界面点击「申请发信权限」按钮,创建待管理员处理的发信权限记录
  3. 无限制发送:以下方式可以跳过余额检查:
    • 在 admin 后台将地址加入「无限制发送地址列表」
    • 配置 NO_LIMIT_SEND_ROLE 环境变量,指定可以无限发送的用户角色

Note

DEFAULT_SEND_BALANCE 仅在地址尚无 address_sender 记录时自动插入初始额度(ON CONFLICT DO NOTHING已有记录包括管理员禁用或手动设置的行一律保持原样runtime 不会修改;历史异常或被禁用的地址需由管理员在后台手动启用并设置余额。

第 1 层 verifiedAddressList 命中时不扣余额,但同样计入发信额度;第 2/3/4 层统一扣 balance。

发信额度对全部发信渠道生效admin 发信接口也会一起计入。

每日和每月额度按 UTC 时间窗口计算。

当前额度实现属于 soft guard,适合日常额度控制;在数据库异常或高并发场景下,它不适合作为绝对严格的成本硬闸。

给 Cloudflare 上已认证的转发邮箱发送邮件

适合未完成 Email Routing onboarding 的域名,或 Workers 免费版。

只有收件人在 admin 后台的 已验证地址列表 中时,才会通过 SEND_MAIL binding 发信。