作者使用 Cloudflare Email Routing 接收域内邮件,经 Worker 分类后写入 D1,再通过 CLI 和守护进程供 Agent 消费,并新增网页邮箱。设计重点是先持久化邮件与处理状态,将 D1 作为持久队列。
我经营着一家只有自己一个人的软件公司,大部分工作都由 AI Agent 完成。这有一个出乎意料、又相当烦人的前提:Agent 得能收邮件。
注册验证码、平台通知、付款收据、客户回复,全都通过邮件送达。而我的大部分自动化流程,都卡在了“得有人打开收件箱,把验证码复制出来”这一步。用脚本轮询 IMAP 也能解决,但它不稳定、需要维护状态,而且每个 Agent 都得有自己的凭据。
于是,我做了一个内部邮件中枢。这周,我又给它加了一个 Webmail 客户端,而这恰恰成了有意思的部分,因为整套系统都跑在 Cloudflare 的免费套餐上。下面介绍它的架构、几个关键决策,以及四个值得借鉴的避坑经验。
MX (Cloudflare Email Routing)
└─> Email Worker one entry point for every address on the domain
├─ classify verification / platform / spam / human
└─ D1 every message persisted = durable queue
├─ CLI the agent-facing interface (drain, wait, cred, triage…)
└─ daemon callback per category
真正撑起这套系统的是三个特点:
用 D1 做队列,而不是内存。Worker 在做任何其他事情之前,都会先把邮件、分类结果和处理状态写入 D1。无论我的笔记本关机、Agent 运行到一半崩溃,还是我重新部署 Worker,邮件都还在。后续每一步操作,无论是 CLI drain、daemon 回调,还是标记 done/ignore,都是某一行数据的状态转换,而不是一个可能错过的事件。
收件时分类,而不是读取时分类。邮件进入队列之前,Worker 就会将其标记为 verification、platform、spam 或 human。正是这个决定,让 CLI 真正好用:索要登录验证码的 Agent 不必读取营销邮件,清理垃圾邮件时也不会碰到真正的客户回复。
CLI 才是 Agent 真正想要的接口。20 条命令,含义明确的退出码:
easy-mail drain # what arrived since the last session
easy-mail wait --timeout 60 # block until a verification mail lands
easy-mail cred <selector> # extract the code / signed link from it
easy-mail triage --apply # apply the rule set to the queue
easy-mail done 12 15 19 # mark processed
对 Agent 来说,一次返回“0 封新邮件,退出码 0”的工具调用,远比一个需要处理分页、还返回半页 JSON 的 API 有价值。退出码就是接口契约:0 表示无事可做或已完成,1 表示需要关注,2 表示用法错误。
在此之前,这个中枢只供 Agent 使用。要阅读真人发来的邮件,我还是得打开邮件客户端。于是,我给中枢加了一个 Web 客户端:SPA 部署在 Pages 上,API 由 Worker 提供,数据存储在 D1 中。不需要付费的 Workers 套餐,也不需要 Access 席位。
访问模型刻意保持简单,也刻意没有采用单点登录:
一个邮箱 = 一个账号 + 一个密码。不是登录一次就能看到所有邮件地址。每个人只能访问自己的邮箱。
密码使用 PBKDF2-SHA256,迭代 10,000 次,每个账号都有独立的盐值,绝不以明文存储。
会话 Cookie 由 base64url(mailbox|expiry) 加上 HMAC 签名组成,无状态,也就没有需要维护的会话表。
读取或写入其他邮箱时,返回 404,而不是 403:攻击者甚至不应该知道对应的数据行存在。
独立的 /admin 控制台使用 Bearer token,而不是用户会话,可以开通账号、重置密码、禁用账号,以及跨邮箱读取邮件。每项操作都会记录到审计表中。
第一版使用的全局 Webmail 共用密码已经删除,而不只是标记为弃用。
我用唯一真正算数的方式测试了隔离性:准备五个装有真实邮件的真实邮箱,邮件数量分别为 74 / 5 / 46 / 68 / 82 封,逐一登录,确认无法读取其他任何邮箱;另外还直接探测 URL,确认返回的是 404,而不是身份验证错误。
Cookie 是跨站场景的陷阱。如果 SPA 部署在 mail.example.com,而 API 部署在 api.example.com,那么从页面的角度来看,会话 Cookie 就是第三方 Cookie,浏览器会悄悄拒绝存储它。把 API 挂到同一主机下的一个路由上,只改一行路由就解决了问题,省去了整整一周调试 SameSite 的工夫。
Turnstile 也会拦住你自己的自动化。注册表单一旦开启机器人防护,你自己的程序化自测就跑不通了。给 Agent 保留一条经过身份验证的 API 路径,把浏览器表单当作只供人类使用的入口。
附件默认就只有文件名,除非你决定额外处理。Email Routing 会提供邮件正文;要持久化附件的字节数据,就得再多走一步:回到 IMAP 获取,或者在收件时直接存入 R2。尽早做决定,后面再补上“下载附件”的功能,就意味着要修改 schema,还得回填历史数据。
免费套餐一封邮件也发不出去。Cloudflare Email Routing 只负责收件。发件要么使用付费邮件发送产品,要么接入事务邮件服务商,Resend 的免费额度是每天 100 封。所以,坦白说,我的邮件中枢目前就是:什么邮件都能收,什么邮件都不能发。UI 里没有写邮件按钮,我也不会上线一个假装能用的按钮。
是 CLI 的接口契约。数据库和 UI 都不是关键。当接口提供的是“列出新邮件、阻塞等待验证码到达、提取验证码、标记完成”这几个操作时,Agent 只需四次工具调用就能驱动整个流程,无须推理邮箱当前处于什么状态。这套系统里的其他所有部分,都是围绕这个契约搭建的配套设施。
如果你正在把 Agent 接入现实世界的系统,我的建议是:先让队列具备持久性,尽早完成分类,再用带退出码的 CLI 提供接口。Web UI 可以最后再做。我就是这么做的,而到那时,我已经非常清楚 UI 需要展示什么了。
我在 pinnadash.com 开发小型 Windows 实用工具,以及它们背后的自动化系统。这个邮件中枢是内部工具,不是产品。Webmail 客户端运行在 mail.pinnadash.com,只供我们自己的邮箱使用,必须登录才能访问。如果你想从另一个角度聊聊“让 Agent 接收邮件”这个问题,比如哪里会出故障、重来一次我会怎么做,欢迎在评论区交流。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。