域名邮箱转到 Gmail 被丢垃圾邮件?用 Gmail API 把邮件直接写进收件箱

13 min

从转发到 Gmail 说起

我的域名邮箱一直用 Cloudflare Email Routing 收信,最开始直接转发到 Gmail。配置很简单,用了一阵才发现很多正规平台的邮件都没收到:npm 的验证码、HSBC 的通知,甚至 Apple 的邮件。Cloudflare 的日志显示已经转发成功,最后在 Gmail 的垃圾邮件里找到了它们。

问题出在转发这个动作上。Cloudflare 收下邮件后,会用自己的服务器把它再投递一次给 Gmail。在 Gmail 看来,这封邮件自称来自 HSBC,却是 Cloudflare 的服务器送来的。Gmail 只能靠原始的 DKIM 签名和它对这条链路的信任来判断真假,判断不准的就进了垃圾邮件。

换成 mail2telegram 之后

后来我换成了 mail2telegram,这篇文章的旧版本写的就是它的部署教程。邮件交给 Cloudflare Worker 处理,直接推送到 Telegram Bot。为了长期保存,我还配了 FORWARD_LIST,让它把邮件顺便备份到 Gmail。

这个备份在 Worker 里调用的是 message.forward(),本质上还是 Cloudflare 把邮件再投递一遍,和最开始的转发走同一条路。Telegram 这边的问题解决了,备份那边的老问题还在。

让我决定换掉它的是 Steam 的邮件。大概是 Steam 发信那边的某项配置有问题,这些邮件在转发时直接被 Cloudflare 拦了下来,Gmail 里根本收不到。以前进了垃圾箱,至少还能翻出来,这回连备份都没有。

只要备份还靠「再发一次」,就得看中间每一环的脸色:从发件方的认证配置到 Cloudflare 的检查,再到 Gmail 的反垃圾策略,哪一环不认,邮件就到不了。所以我决定备份这一步干脆不发信。

换个思路:用 Gmail API 直接写入

Gmail API 有一个 users.messages.insert 接口,官方文档是这么描述的:

Directly inserts a message into only this user’s mailbox similar to IMAP APPEND, bypassing most scanning and classification. Does not send a message.

意思是它和 IMAP 的 APPEND 类似,把一封邮件直接放进这个用户自己的邮箱,跳过大部分扫描和分类,也不会发出任何邮件。

这正是我要的。Worker 收到邮件后拿到原始字节,用 OAuth 授权调用这个接口,把邮件写进 Gmail 收件箱。整个过程没有 SMTP 投递,没有 SPF、DKIM 要过,Gmail 也不会像对待外来邮件那样审查它,拒收和进垃圾箱都无从谈起。

写进去的是完整的原始邮件,邮件头、正文和附件都在,在 Gmail 里打开和正常收到的邮件一样。邮件时间取自它自己的 Date 头,在收件箱里的排序也是对的。

Synopsis

按这个思路,我从零写了 Synopsis,运行在 Cloudflare Workers 上,已经开源:

lainbosynopsis

Loading repository data...

-- -- --

它把域名邮件完整备份到 Gmail,同时在 Telegram 私聊里发一条中文摘要。

flowchart TD
    A[域名收到邮件] --> B[Cloudflare Email Routing 交给 Worker]
    B --> C[Gmail API 写入原始邮件]
    C -- 成功 --> F[AI 生成中文摘要并推送到 Telegram]
    C -- 失败 --> D[依次尝试 Cloudflare Email Sending 和 Resend 发送原件]
    D -- 成功 --> F
    D -- 都失败 --> E[Telegram 告警,Cloudflare 稍后重新投递]

Telegram 里能做什么

每封邮件到了之后,Bot 会发来一条摘要:

  • 第一行写着这封邮件发到了哪个地址。给不同网站注册时用不同的别名,一眼就能看出是哪家来的信。
  • 验证码邮件会把验证码单独放一行,点一下就能复制。
  • 其他邮件是一段 30 到 60 字的总结,最后一行用 🟢、🟡、🔴 标出需不需要处理、有多急。邮件里有退订入口的,还会顺带提醒可以退订。
  • 点「原文」可以在 Telegram 里直接看正文,看完点「返回摘要」切回来。
  • 点「删邮件+消息」会把 Gmail 里的这封邮件移进垃圾箱,同时删掉这条 Telegram 消息,适合处理用完就没用的验证码邮件。

摘要可以用三种 AI 服务,任选一种:OpenAI 兼容接口、OpenRouter 和 Gemini。默认提示词会把描述性的内容都转成简体中文,验证码、金额和单号这类需要原样复制的内容保持不变。

备份优先

写这个项目时,我让原件备份排第一,摘要和通知晚一点也没关系。

  • Gmail 写入失败时(比如授权过期),会依次尝试 Cloudflare Email Sending 和 Resend,往另一个邮箱发一封带 original.eml 附件的邮件,同时在 Telegram 告警。这两个兜底都是可选项,但建议至少配一个。
  • AI 出错或者 Telegram 发送失败,都不会影响已经完成的备份。摘要失败时 Bot 会发一条占位消息,说明原件已经备份。
  • 通知没发出去的邮件,定时任务每 5 分钟检查一次,按记录的状态补发。

兜底走的还是发信那条路,只在 Gmail API 出问题时临时顶上,日常备份靠的一直是 Gmail API 写入。

部署

完整步骤和每个配置项的说明都在仓库的 README 里,这里只讲大致流程和几个容易踩坑的地方。

准备

  • 一个接入 Cloudflare 的域名,会用到 Email Routing、Workers 和 KV
  • 一个用来收备份的 Gmail 账号,以及你自己在 Google Cloud 创建的 OAuth 客户端
  • 一个 Telegram Bot
  • OpenAI 兼容接口、OpenRouter、Gemini 三选一的 API key
  • Node.js 22 以上和 pnpm

1. 安装依赖,创建 KV

git clone https://github.com/lainbo/synopsis.git
cd synopsis
pnpm install --frozen-lockfile
cp wrangler.example.jsonc wrangler.jsonc
cp .dev.vars.example .dev.vars
pnpm exec wrangler login
pnpm exec wrangler kv namespace create MAIL_KV
pnpm exec wrangler kv namespace create MAIL_KV --preview

把得到的两个 KV ID 填进 wrangler.jsonc。

2. 拿到 Gmail 的 refresh token

这是整个部署里最麻烦的一步。

  1. 在 Google Cloud Console 新建项目,启用 Gmail API。
  2. 在 Google Auth Platform 里配置应用,受众选 External,然后把发布状态切到 Production。一直停在 Testing 状态的话,refresh token 通常 7 天就会过期,备份也就跟着断了。
  3. 创建 Web application 类型的 OAuth 客户端,授权重定向 URI 填 https://developers.google.com/oauthplayground。
  4. 打开 OAuth Playground,在右上角的设置里勾选 Use your own OAuth credentials,填入刚才的 client ID 和 client secret,Access type 选 Offline。
  5. 在 Step 1 填入下面两个 scope 并授权,账号选用来收备份的那个 Gmail:
https://www.googleapis.com/auth/gmail.insert
https://www.googleapis.com/auth/gmail.modify
  1. 在 Step 2 点 Exchange authorization code for tokens,保存 refresh_token。

gmail.insert 用来写入邮件,gmail.modify 用来查询邮件状态和移进垃圾箱,Telegram 里的删除按钮靠的就是它。

3. 创建 Telegram Bot

在 BotFather 里用 /newbot 创建 Bot,保存好 token。然后用自己的账号给 Bot 发一条 /start,再读出自己的私聊 ID,仓库的 Telegram 设置文档 里有现成的脚本。最后随机生成一串字符作为 TG_WEBHOOK_SECRET,只能用字母、数字、下划线和短横线。

4. 写入 Secrets,选择 AI 服务

生产环境的密钥都用 wrangler secret put 写入,不要写进配置文件:

pnpm exec wrangler secret put GMAIL_CLIENT_ID
pnpm exec wrangler secret put GMAIL_CLIENT_SECRET
pnpm exec wrangler secret put GMAIL_REFRESH_TOKEN
pnpm exec wrangler secret put TG_BOT_TOKEN
pnpm exec wrangler secret put TG_CHAT_ID
pnpm exec wrangler secret put TG_WEBHOOK_SECRET

AI 服务在 wrangler.jsonc 的 vars 里选择。以 Gemini 为例:

{
  "SUMMARY_PROVIDER": "gemini",
  "GEMINI_MODEL": "你要用的模型 ID"
}

再写入对应的密钥:

pnpm exec wrangler secret put GEMINI_API_KEY

选 OpenAI 兼容接口或 OpenRouter 时,把变量名换成 OPENAI_* 或 OPENROUTER_* 就行。用第三方的 OpenAI 兼容服务,还要填 OPENAI_BASE_URL。

兜底发信(Cloudflare Email Sending、Resend)的配置见 README 的「邮件兜底」一节。

5. 部署,接上 Telegram 和邮件路由

pnpm run deploy

部署完成后:

  1. 按 README 注册 Telegram webhook,地址是 https://<你的 Worker 地址>/telegram/webhook,secret_token 填刚才的 TG_WEBHOOK_SECRET。
  2. 在 Cloudflare 控制台打开域名的 电子邮件路由,把 Catch-all(或者某个指定地址)的操作改成 发送到 Worker,选择 synopsis。
  3. 往域名邮箱发一封普通邮件和一封带附件的邮件,确认 Gmail 收件箱里有原件、附件能打开,Telegram 收到了摘要,「原文」和「返回摘要」都能用。

使用前要知道的

  • 只支持一个 Gmail 账号,Telegram 也只能本人私聊使用。
  • Telegram 里的原文是纯文本,长邮件会截断,完整排版和附件要去 Gmail 看。
  • 生成摘要需要把邮件内容发给你选的 AI 服务,验证码之类的敏感信息也在里面,选服务商时要考虑这一点。
  • 正文缓存默认保留 7 天,过期后 Telegram 里的按钮就用不了了,邮件本身还在 Gmail 里。
  • 「返回摘要」会重新调用一次 AI,会产生额外费用。

总结

换成 Synopsis 之后,域名邮件都会出现在 Gmail 收件箱里,包括之前被拦下的 Steam 邮件。Telegram 负责第一时间告诉我来了什么邮件、要不要处理,Gmail 负责长期保存。

如果你也在用 Cloudflare Email Routing 转发到 Gmail,又被垃圾邮件或者转发失败困扰,可以试试这个项目。