跳到正文

用 Cloud Mail 搭域名邮箱,收信之后还有归属问题

记录 Cloud Mail 域名邮箱的部署过程,讲清 Cloudflare 收件、D1 与 KV 存储、Resend 发件,以及先收信后建邮箱的历史邮件归属问题。

归入领域
产品基础设施
阅读时间
13 分钟阅读
发布于
2026.10.12

一句话结论

Cloud Mail 可以把域名收件、网页邮箱和 Resend 发件接起来,但 catch-all 收到的未知地址邮件需要单独处理归属,日常发布也要保护已有资源绑定和数据。

工作室网站搭好以后,我接着给它配了一套域名邮箱。最后用的是开源项目 Cloud Mail,网页和 API 放在 Cloudflare Workers,收件交给 Email Routing,发件用 Resend。

2026 年 10 月 8 日做到一半,邮箱工作先暂停了。第二天继续,把登录、外部收件和域名回复跑通,还补了一个原来没有的邮件认领功能。

这次最值得记下来的问题,是邮件已经收进系统,后来建好的邮箱却看不到它。排查到这里,部署就从配域名、填密钥,变成了处理邮件到底属于谁。

下面按这次实际用过的方案来写。域名和邮箱地址统一替换成 example.com 下的示例,配置中的资源标识也不公开。文中记录的是这次部署的结果,不保证后续上游版本仍采用相同实现。

收件、网页和发件分别配置

我用的方案把几件事分开了。

部分这次使用的服务负责的事情
网页和 APICloudflare Workers提供网页邮箱、登录及邮件操作接口
域名收件Cloudflare Email Routing 与 Email Worker将来信交给应用解析和保存
邮件数据D1保存正文、元数据和邮件归属
附件KV 回退方案保存附件内容
发件与回复Resend用经过验证的域名身份发出邮件

这里有两个容易混在一起的地址。mail.example.com 是网页入口,user@example.com 是邮箱地址。网页自定义域名负责 HTTPS 访问;邮件能不能收到,还要看根域 MX 和 Email Routing 的配置。

我给收件配置了 catch-all,将这个域名的来信交给 Email Worker。已有邮箱收到的邮件,按对应账号保存。对于还没创建的地址,这次启用了接收未知收件人的设置,让邮件先留在管理页面的“无人收件”里。

catch-all 的意义到这里就很具体了。一个新前缀可以先收到邮件,存储和请求仍然受服务额度限制。这套配置也需要有人处理无人收件列表,不能配完以后就一直放着。

R2 没开通,附件先用 KV

最初检查 Cloudflare 资源时,R2 API 提示尚未启用,需要到后台开通。当时邮箱工作暂停,恢复部署后也没有启用 R2。

我检查了这版代码的存储选择。应用会先看 S3 配置,再看 R2 绑定,两者都没有时走 KV。因此这次让邮件正文和元数据进 D1,附件使用 KV 回退,收发所需的功能仍然可以继续验证。

这个选择只覆盖了眼下的使用需求。附件长期保存多少、怎样备份、何时清理,当时都没有定下来。自动删除也保持关闭。后续如果要换成 R2,需要另外检查已有附件的读取和迁移,不能只加一个绑定就认定旧数据已经搬过去。

Resend 的认证记录要和收件 MX 分开

收件配置完成以后,回复邮件还要单独配置 Resend。应用里填了发信 key,也仍然需要完成域名认证和启用发件设置。

这次保留了根域用于 Cloudflare 收件的 MX,另外添加 Resend 要求的记录。下面只列用途,具体值以自己账户后台生成的内容为准。

记录示例名称用途
TXTresend._domainkey.example.comResend 的 DKIM 认证
MXsend.example.comResend 要求的发件反馈记录
TXTsend.example.comSPF 认证
CNAMErsend.example.com本次后台要求的附加记录

send.example.com 的 MX 和根域 MX 是不同名称下的记录,可以按各自用途配置。照着发件服务的后台填 DNS 时,要看清主机名,保留已有的根域收件记录。

发信 key 用的是仅允许指定域名发送的专用 key。凭据留在应用设置和受限配置中,JWT 签名密钥放在 Worker Secrets,文章和仓库都不保存这些值。

10 月 9 日,Resend 域名验证通过,发件配置启用。外部客户端也收到了测试邮箱的域名回复。那次没有逐项核对邮件认证头和投递事件,因此验收结果只写到已收到回复,不延伸成所有收件服务都能稳定进主收件箱的保证。

先收信,后建邮箱,旧邮件留在原处

这次遇到的具体情况,是向一个尚未创建的地址发送测试邮件。catch-all 把邮件收了下来,管理员能在“全部邮件 → 无人收件”中找到它。

随后创建这个地址对应的邮箱,打开收件箱,旧邮件没有出现。

沿着代码看,邮件到达时就已经保存了归属。找到已有账号时,记录写入对应的用户和邮箱 ID;地址还没有账号时,归属 ID 为 0。后来创建邮箱,只新增账号,不会回头修改这些邮件。

这也解释了为什么“全部邮件里能看到”和“我的收件箱里能看到”会是两个结果。普通收件箱按归属查询,历史邮件还处在无人收件状态。

我最后补了一个管理员认领功能。操作顺序很短。

  1. 创建与原收件地址一致的邮箱。
  2. 在无人收件列表打开那封旧邮件。
  3. 点击“认领到邮箱”,选择匹配的目标并确认。

认领以后,邮件离开无人收件列表,出现在目标邮箱的收件箱,附件也一起归属过去。邮件正文和未读状态保持原样。

认领功能不能只改邮件表

这个功能的界面只有一个按钮,后端要检查的条件不少。

先看权限。系统已有“查看全部邮件”的委派权限,认领会改变邮件归属,所以单独限制为管理员操作。能查看全部邮件的账号,不自动拥有移动归属的权限。

再看地址。目标邮箱必须匹配原始 SMTP 信封收件地址,忽略大小写。邮件内容里的 To 标头不能代替这个判断,应用实际接收这封邮件的地址才是匹配依据。

应用原有的收件流程支持 plus 别名,在没有精确邮箱时尝试对应基础邮箱。这次新增的历史邮件认领暂时只做地址精确匹配,没有把别名规则一起扩进去。

附件也有归属字段。如果只更新邮件,正文可能已经进入新收件箱,附件却仍然留在原状态。我把邮件和普通附件、内联附件的归属更新放进同一个 D1 事务中,有一项失败就回滚。

更新语句还要再次检查邮件是否仍然无人收件、目标账号是否仍然有效。读取和写入之间,另一个管理员可能已经完成认领,账号也可能被停用。前面查过一次,不足以保证写入时条件还成立。

这次为认领功能跑了 10 项测试,覆盖管理员权限、地址匹配、附件转移和事务回滚,也检查了并发和参数错误。发布后确认未登录请求被拒绝,再在页面上确认旧邮件归属成功。这个改动没有新增数据库字段,也没有重建已有数据。

发布配置要和上游默认值分开

部署时,我单独保留了 wrangler.personal.toml,把自己的 D1、KV、静态资源目录和网页域名绑定放在里面。这样更新上游代码时,可以明确看到个人部署需要保留哪些配置。

下面是这次项目的更新发布顺序。路径使用相对目录,命令针对已经初始化好的实例;首次部署还需要先创建资源、填写绑定、配置 Secrets 和邮件路由。

# 在 cloud-mail 仓库根目录开始,前端发布构建输出到 mail-worker/dist。
cd mail-vue
pnpm install --frozen-lockfile
pnpm run build

cd ../mail-worker
pnpm install --frozen-lockfile

# 这是本次 fork 新增的认领测试,需要支持 node:sqlite 的 Node。
pnpm run test:claim

# 显式指定个人配置,使用已经确认的资源绑定。
pnpm exec wrangler deploy --config wrangler.personal.toml

test:claim 是这次修改增加的脚本,上游版本未必有它。前端产物的位置来自这版发布环境配置,换一个版本也要重新核对。

数据库已经初始化,普通更新发布不重放初始化 SQL,更不执行清空操作。代码推到 GitHub 后,这次仍然执行了 Wrangler 发布,再检查线上 Worker 和网页资源,才把改动记为已经部署。

网页返回 HTTP 200 只能证明页面可访问。收件要用外部邮件检查,回复也要去外部收件箱确认。无人收件和历史认领则分别检查,才能知道问题出在哪一段。

邮箱能用了,客户端接入还没有做

收发之外,这次还调整了个性化设置页面,加上登录效果预览和面板透明度滑块,让背景、标题的修改更容易看清结果。前端构建通过,桌面和手机宽度下也做了本地验证。

当时确认完成的事情,包括管理员登录并修改初始密码、已有地址外部收件、未知地址进入无人收件,以及认领后在目标邮箱查看旧邮件。外部客户端收到的域名回复,也补上了发件验证。

我使用的入口仍然是网页邮箱。这次没有配置 IMAP、POP3 或 SMTP 客户端接入,网页地址和 Resend API key 都不能拿来当邮件客户端的登录参数。

公开注册保持关闭,转发、AI 提取和自动删除也没有启用。后续需要处理的,是附件与 D1 的备份、保留策略,以及是否真的需要客户端接入。至于规划中的联系和售后地址,要先创建并验证,再替换网站上的联系方式。

参考资料

继续阅读

2026
  1. 做一只不催你的桌面宠物

    产品设计 · 桌面宠物 · macOS

2025
  1. 今天成为了 MooTool 贡献者

    Contributors · GitHub · Tools

  2. 多环境下的 Git Config 配置

    Git · 开发习惯 · Open Source