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

173 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 配置发送邮件
::: 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_TOKEN``RESEND_TOKEN_<DOMAIN>` 已配置 | Resend API | 是 |
| 3 | `SMTP_CONFIG` 含当前域名配置 | worker-mailer SMTP | 是 |
| 4 | `SEND_MAIL` 已绑定(以上均未命中) | **Cloudflare binding推荐主通道** | 是 |
| — | 以上均未命中 | 抛错 | — |
> [!NOTE]
> binding 发信失败会直接报错。
## 使用 Cloudflare `send_email` binding推荐
仅 CLI 部署时使用,在 `wrangler.toml` 中添加:
```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` 下面
```bash
# 切换到 worker 目录
cd worker
wrangler secret put RESEND_TOKEN
```
如果你有多个域名,对应不同的 `api key`,可以在 `wrangler.toml` 中添加多个 secret, 名称为 `RESEND_TOKEN_` + `<. 换成 _ 的 大写域名>`,例如
```bash
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](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
> [!warning] 重要
> JSON 中的 key如下面示例中的 `your-domain.com`)必须替换为**你自己的域名**,即 `DOMAINS` 变量中配置的域名。
> 这是最常见的配置错误之一,请勿直接复制示例中的域名。
```json
{
"your-domain.com": {
"host": "smtp.example.com",
"port": 465,
"secure": true,
"authType": [
"plain",
"login"
],
"credentials": {
"username": "your-smtp-username",
"password": "your-smtp-password"
}
}
}
```
**字段说明:**
| 字段 | 说明 |
|------|------|
| key`your-domain.com` | 你的发信域名,必须与 `DOMAINS` 中配置的域名一致 |
| `host` | SMTP 服务器地址,如 `smtp.mailgun.org``smtp.gmail.com` 或你自建的 SMTP 服务器地址 |
| `port` | SMTP 端口,通常 `465`SSL`587`STARTTLS |
| `secure` | 是否使用 SSL/TLS端口 465 时设为 `true`,端口 587 时设为 `false` |
| `authType` | 认证方式,一般使用 `["plain", "login"]` |
| `credentials.username` | SMTP 服务器的登录用户名 |
| `credentials.password` | SMTP 服务器的登录密码 |
如果你有**多个域名**使用不同的 SMTP 服务,在同一个 JSON 中添加多个 key 即可:
```json
{
"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` 下面
```bash
# 切换到 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 发信。