feat: 通过 Cloudflare Hyperdrive 支持外部数据库邮件存储

Author: dreamhunter2333Created Sep 14, 2026Updated Sep 14, 2026

目标

支持通过 Cloudflare Hyperdrive 将邮件存储到部署者自己的外部数据库,降低邮件数据对 D1 容量的依赖;保持默认 D1 部署、现有 API 和客户端兼容。

Hyperdrive 是数据库连接加速/连接池服务,数据实际存放在外部数据库中。官方支持 PostgreSQL、MySQL 及兼容平台。建议首期实现 PostgreSQL(例如 Neon、Supabase 或自建 PostgreSQL),MySQL 作为后续独立适配,避免首期同时维护三种 SQL 方言。

当前实现分析

基于 maincea2a8c7

  • db/schema.sql:收件存于 raw_mails,包含原文 raw、gzip raw_blob、metadata、已读状态;发件记录存于 sendbox
  • worker/src/email/storage.tsstoreRawMail() 直接使用 D1,依赖 PRAGMA table_infoD1Resultemail/index.ts 使用 meta.last_row_id 生成后续 webhook 所需邮件 ID。
  • 邮件读取、删除、已读状态分散在 mails_api/user_api/admin_api/telegram_api/,解析接口及 webhook 附件下载也直接访问 D1。
  • user_api/user_mail_api.ts 通过 users_address → address → raw_mails JOIN 实现账户聚合收件箱和删除鉴权;地址列表包含邮件数子查询。
  • common.tsadmin_api/address_api.tsscheduled.ts 的地址删除、未知邮箱邮件清理、空邮箱清理均与邮件表关联。
  • email/ai_extract.tsmessage_id 更新 metadata;send_mail_api.ts 单独写入发件箱。

因此,仅修改收信写入或将 env.DB 换为 Hyperdrive binding 无法完成支持。

建议方案

1. 存储边界

首期将 raw_mailssendbox 完整放在所选邮件后端;账户、地址、权限、设置等继续保留 D1。每次部署只配置一个活动邮件后端,默认 d1

新增小范围的邮件存储接口,提供保存、按 ID/地址读取、列表/计数、已读更新、metadata 更新、删除与批量清理。实现 D1 和 PostgreSQL 两个适配器;不模拟完整 D1 API,也不做全项目 ORM 重写。

业务层返回统一的保存结果(如 { success, id }),消除 D1Result/meta.last_row_id 泄漏;适配器统一 ID、时间、二进制数据和 count 的返回类型。所有邮件入口必须走同一后端,包括内部邮件、发件箱、Telegram、webhook 测试与附件下载。

备选方案是只外置邮件正文,在 D1 保留索引和状态,能够减少 JOIN 改造,但会引入正文与索引双写、孤儿清理,且 D1 仍随邮件数量增长。如调整为该方案,应明确它仅解决正文容量问题。整库迁移不纳入本期。

2. Hyperdrive 与 PostgreSQL 接入

建议新增配置(名称待实现时确认):

toml
[vars]
MAIL_STORAGE_BACKEND = "postgres" # 默认 d1

[[hyperdrive]]
binding = "MAIL_HYPERDRIVE"
id = "<hyperdrive-config-id>"
  • worker/src/types.d.ts 声明可选 Hyperdrive binding;使用官方支持版本的 pg,从 binding 的 connectionString 创建客户端。模板已有 nodejs_compat
  • 客户端在请求/邮件事件内创建并可靠释放,避免跨事件复用连接;明确连接超时及外部数据库连接数预算。
  • 邮件连接关闭 Hyperdrive 查询缓存:官方说明缓存默认开启,写入不会使已有读缓存失效。否则收信轮询、已读更新、删除后读取可能返回旧数据。
  • 选择 PostgreSQL 却缺少 binding 或 schema 不匹配时明确报错;不静默回退写 D1,避免数据分散。
  • 数据库凭据交由 Hyperdrive 配置管理;本地连接字符串只放本地环境,不提交到仓库。

3. Schema 与 SQL 适配

  • 单独维护 PostgreSQL 初始化和版本迁移,不能复用 SQLite 的 PRAGMAAUTOINCREMENTdatetime()? 占位符。
  • 原文使用 TEXT、压缩内容使用 BYTEA;兼容现有 gzip 数据以及 ENABLE_MAIL_GZIPENABLE_MAIL_READ_STATUS 行为。
  • 使用 INSERT ... RETURNING id;保留迁移前 ID,并正确推进序列。若使用 bigint,明确安全整数范围和序列化策略,避免破坏客户端、邮件链接及 webhook 签名。
  • 明确 UTC 时间及 API 输出格式;至少覆盖地址+ID分页索引、清理时间索引、message_id 索引。
  • metadata 更新优先使用已保存的邮件 ID,避免仅按非唯一 Message-ID 更新其他收件人的邮件。
  • 保留现有附件移除配置语义;外部存储不自动解除 Worker 的运行资源或入站邮件限制。

4. 跨库查询和删除一致性

  • 用户收件箱:先由 D1 获取当前用户有权访问的地址,再以这些地址约束外部数据库查询;邮件详情、删除等同样保留所有权校验。
  • 地址列表统计:针对当前页地址批量查询收/发件数并合并,避免 N+1;跨地址列表需在外部数据库统一排序分页,不能分别分页后拼接。大量地址的参数/批次上限需明确。
  • 未知地址邮件和空邮箱清理无法继续使用跨库 JOIN/NOT IN:分页扫描候选地址,批量向另一侧查询存在性,并在删除前复核。确保 count 与列表筛选一致。
  • 删除邮箱横跨 D1 和 PostgreSQL,没有统一事务。需要可恢复的删除流程:先记录待删除状态并阻止新写入,再分批幂等清理邮件、完成 D1 删除;中断后可以重试,避免部分成功、孤儿邮件或地址重用造成数据误归属。

5. 失败语义与历史数据迁移

  • 当前收信存储异常分支仅记录日志并继续处理;接入外部数据库时需明确持久化失败行为,不能让邮件未保存却表现为处理成功。
  • MVP 不引入运行时双写。写入失败明确失败;若增加重试,应使用稳定的投递幂等键,不把可能缺失或重复的 Message-ID 当成全局唯一键。
  • 提供可断点续传的 D1 → PostgreSQL 迁移工具,保留收/发件 ID、时间、原文/压缩内容、metadata、已读状态;以旧 ID 幂等导入。
  • 推荐维护窗口切换:暂停邮件写入及邮件修改/清理 → 迁移并校验记录数、内容摘要和压缩解码 → 校准序列 → 切换后端 → 恢复流量。
  • 保留 D1 备份;启用外部后端后产生的新邮件、修改和删除必须先回迁/对账,不能只改配置就宣称无损回滚。

实施拆分与验收

  • 抽取邮件存储接口,D1 默认行为保持兼容。
  • 实现 PostgreSQL schema、迁移、Hyperdrive binding 和连接生命周期管理。
  • 覆盖收/发件全部读写入口、账户聚合收件箱、统计、清理及 webhook/Telegram。
  • 完成跨库权限检查与可恢复的邮箱删除流程。
  • 验证两个后端的 API 兼容性:收信立即可见、删除/已读立即生效、gzip、metadata、附件、分页、发件记录。
  • 验证跨用户访问拒绝、未知地址/空邮箱清理、数据库不可达及超时、写入失败处理、迁移重复执行和切换回滚。
  • 本地 PostgreSQL 集成验证之外,在实际 Hyperdrive 环境验证 email() 和 HTTP 路径、关闭缓存效果及连接释放。
  • 更新中英文 Worker 配置/部署/迁移文档,以及实现时当前 (main) 中英文 CHANGELOG。

参考资料

Source: dreamhunter2333/cloudflare_temp_email