博客

自建邮件系统这十一年:从 Mailgun 到 Cloudflare

Cover Image for 自建邮件系统这十一年:从 Mailgun 到 Cloudflare

我想要一个真正属于自己的邮箱,这个念头从 2015 年就有了,直到 2026 年才算真正落地——中间横跨十一年,换过至少八种方案。从给自己的产品找 EDM 供应商开始,一路试过 Mailgun、群晖 NAS 上的独享 IP 邮件服务器、腾讯和网易的企业邮箱、Zoho,最后靠 Cloudflare Routing 转发到 Gmail、再挂一个 Resend 当发件身份,勉强拼出一套"看起来是自定义域名"的系统——但 Gmail 始终是那个真正的邮箱,这套拼凑方案带来的提心吊胆,最终变成了压垮骆驼的最后一根稻草。这一篇讲的是这十一年里,每一次尝试为什么没能走到底,以及 2026 年 Cloudflare 补上发信能力之后,这块最后的拼图是怎么拼上的。多用户权限、隐私加固、IMAP 客户端支持这些,留到后面几篇再说。

起点:我只是想要一个自己的域名邮箱

事情最早的样子很简单:我想要 me@我的域名 这种邮箱地址,而不是 me@gmail.com。这不只是技术上的虚荣心——官网、名片、合同、跟客户的往来邮件,都应该指向同一个身份,而不是东一个 Gmail 西一个 Outlook。

用 Gmail 或 Outlook 挂自定义域勉强能凑合,但邮件数据始终躺在别人的服务器里。域名在我自己手里,收发这条链路我也想留在自己能掌控的范围内——这个需求从 2015 年就一直存在,中间断断续续折腾了十一年,直到今年才算真正落地成一套自己完全掌控的系统。

一开始我把这当成"发信工具"来想。真正做下来才发现,我要的其实是一整套组织级的邮件系统:多域名、多账号、权限分级、配额、审计日志——只是规模远远够不上 Google Workspace 那种几百上千席位的量级。这个认知转变,是这十一年里最值得记录的东西。

2015 年:从 EDM 供应商调研开始

最早的起点其实不是"我想要个人邮箱",而是 EDM(Electronic Direct Mail,营销邮件)。那几年我同时也是个站长,自己的产品赶上了注册用户爆发式增长的阶段,需要靠 EDM 持续给这些用户发信、做拉新和唤醒——这不是"想要一个邮箱",而是业务规模逼出来的基础设施需求。

我在这个场景下认真比较过 SendGrid 和 Mailgun,SendGrid 用下来不趁手,最后选定了 API-based 的 Mailgun。免费账号能加 5 个域名,我记得很清楚,因为我当时正好把 5 个域名都配了进去,用得好好的。后来账号策略变了,想再加第 6 个的时候被拦住了;更麻烦的是,删掉一个旧域名腾位置,也没法再加新的——那个"5"从"我恰好用满"变成了一道硬墙。

不管是 SendGrid 还是 Mailgun,本质上都是给 EDM 场景准备的纯发信工具——群发、模板、退信统计,一切都是围绕"批量发出去"设计的,天然就是 no-reply@ 的思路:用户不需要回信,我也没打算收。但产品用户量起来之后,光靠 no-reply 单向广播已经不够用了——用户需要一个能真正互动的 contact@ 地址,能回信、能收到用户的反馈和问题,而不是发出去就完事。这个从"单向群发"到"双向联系"的转变,才是"我需要能收信"这个诉求真正冒出来的原因——也是后面群晖那次尝试的直接起因。

同一时期,因为目标是用上自定义域名,我也同步调研过腾讯企业邮箱和网易企业邮箱。网易那边额度给得小,用起来也别扭,很快就出局了。腾讯当时反而相当大方——域名想加多少加多少,随便添加。我在腾讯企业邮箱上停留了一段不短的时间,直到后来某个时间点,腾讯大幅收紧了免费计划下能挂的域名数量;再加上对隐私的顾虑逐渐积累,最后还是弃用了。

这段插曲值得记一笔:免费额度收紧不是 Mailgun 一家的特例,而是几乎所有靠免费引流的邮件服务迟早都会走的路——网易一开始就小气,腾讯是"先大方后收紧",Mailgun 是"先大方后收紧"外加彻底锁死数量上限,三家路径不同,终点却出奇地一致。

2018 年:群晖 DS918P,一次系统化的尝试

因为需要收件能力,2018 年我在群晖 DS918P 上认真搭了一次 MailPlus。这次不是随便在局域网里跑跑——我专门想办法搞定了公网可访问:向 ISP 申请了一个独享的 IPv4 地址,配上动态 DNS,做了一次真正意义上"让邮件服务器活在互联网上"的系统化尝试。

结果还是没扛住。即便有了独享 IP,只要它本质上仍然是一个消费级 ISP 分配出来的地址,出站发信照样大概率进对方的垃圾箱——没有多年积累的发信历史,没有预热,很多主流邮件服务商本身就对住宅/消费级 IP 段抱有天然的不信任,这不是配置层面能改的事。折腾了一阵之后,我把这套方案废弃了。

现在回头看,MailPlus 这类产品的初衷是好的——它假设的是"你有一台机器、一个公网 IP,就应该能搭起一整套邮件服务"。但这个假设成立的前提,其实是回到互联网真正的早期蛮荒年代:那时候没有集中化的信誉体系,没有 Spamhaus 这类黑名单,任何一台联网的机器发信都跟别的机器地位平等。现在的邮件生态早就不是这样了,信誉这件事被牢牢攥在少数几个大厂手里,个人或者小团队从零开始,几乎不可能靠自己一台机器把信誉攒起来。

这些年里我也在不同的云主机上轮番试过自建——最古早的大概是 Linode,后来陆续还用过 Vultr、AWS Lightsail 这些。举这两家当例子,倒不是说非它们不可,只是我自己这些年用过的 VPS 服务商实在太多,挑两个大家都熟悉的说事而已。现在回头看,这其实是一个挺典型的新手误区:搜一下"自建邮件服务器",搜索引擎前几页永远是那套千篇一律的教程——弄一台 VPS,装 Postfix,东配一下 SPF,西配一下 DKIM,看起来每一步都清清楚楚,跟着做就行。但这类教程默认的前提,是互联网还处在"一台服务器就能平等地跟全世界通信"的年代——而实际上早在 2015 年,也就是我第一次认真折腾自定义域名邮件的那个时间点,这盘棋局就已经变了:邮件信誉早就被少数几个大厂垄断,一台新开的 VPS 不管怎么配置 Postfix,本质上都是从零信誉起步,自建这条路径本身已经没什么意义了——只是这些教程没有告诉你这一点,你得自己撞了墙才明白。

这类尝试撞的也是同一堵墙。这不是我一个人的踩坑经历,而是这些云厂商摆在明面上的政策:Vultr 官方文档写得很直白,25 端口默认对所有实例封锁,理由是"频繁被垃圾邮件发送者利用",想解封要提工单,而且新账号的解封请求相当常见地被拒绝或者长期搁置。AWS 这边更彻底——Lightsail 和 EC2 的所有实例默认限制 25 端口的出站流量,解封同样要提交一份说明用途的申请表,官方给出的拒绝理由也高度模板化:账号存在滥用历史、账单状态不良、或者"没有提供清晰的使用场景"——即便是老实巴交刚注册的新用户,也经常收到这种拒信,然后被顺手推荐去用他们自己的 SES。Postfix 装上、DNS 配好、DKIM 也签了,出站照样卡在这道墙前面。

2021 年:Zoho,开箱即用但协议访问要加钱

Zoho Mail 一度是最接近"开箱即用"的选项——免费版能收能发,单域名下带几个用户。但免费版不给 SMTP/IMAP 访问,只能绑定 Zoho 自己的 App 或者网页界面。我想要的是自己写界面、自己掌控数据,而"必须用官方客户端"这件事跟这个诉求是正面冲突的。

2022 年:Cloudflare Email Routing,收件问题第一次被真正解决

2022 年开始用 Cloudflare Email Routing——这是免费的收件转发功能,MX 指向 Cloudflare,规则配好之后直接转发到 Gmail。这一步解决的是"收件"这一半:自定义域名的邮件能可靠地落到 Gmail 收件箱里,不需要自己扛任何服务器。

但发件那一半在当时还没有对应的方案——Cloudflare 那会儿只有 Routing,没有对称的发信能力。

2024 年:Resend 补上发件那一半

2024 年引入 Resend,配置成 Gmail 里的一个"发送方式"账号(Send As),这样从 Gmail 界面里发出去的信,From 地址就是自定义域名,而不是 Gmail 自己的地址。收(Cloudflare Routing 转发到 Gmail)加发(Resend 挂在 Gmail 的 Send As 上)拼在一起,实现了收发两头都落在自定义域名上——代价是,Gmail 本身仍然是那个真正的"邮箱",Cloudflare 和 Resend 都只是在它前面搭的一层身份伪装。

这套组合拳有一个致命的痛点,正是这个痛点让我下定决心一定要彻底自建。只是看信的话没什么问题——谁寄给你、寄到你哪个地址,看内容都是一样的。但一旦要回复,就得万分小心:Gmail 回复邮件时默认用的是你的主 Gmail 地址,而不是这封信原本收到的那个自定义域名地址,除非你在回复框里手动把"发件人"切换成正确的那个 Send As 身份。稍微一着急、一手滑,没看清楚用的是哪个发件地址,回复就已经发出去了——而且没法撤回。我就真的因为这样,用 Gmail 地址回复了一封收件地址明明是自建域名的商务信函,造成过不小的麻烦。这件事之后成了一块永远悬着的心病,每次要回复邮件都要多看一眼发件人选择框,生怕再错一次。这套靠 Gmail 撑起来的系统,也因此只能算是"凑合能用",从来没有真正让我安心过——心理负担太重了。

问题还不只是"要多留意"这么简单,Gmail 的默认回复界面本身就在给这个错误创造条件:

Gmail Reply UI Gmail 回复 UI,默认不显示发件人

点开一封信、按下回复,弹出的紧凑输入框里只显示"回复给谁",发件人这一栏被折叠进右下角那个不起眼的"…"里——不点开,你根本看不到、也改不了这封信即将用哪个身份发出去。对一个手上挂着好几个 Send As 身份的人来说,这个默认视图相当于在暗中鼓励你直接按发送键。

发出去之后想反悔,更是死路一条——邮件的"撤回"是个几乎无解的行业通病,不分中外。SMTP 本身是一套纯粹的存储转发协议,从设计上就没有"撤回"这个概念:邮件一旦被交给对方的邮件服务器,发件人就彻底失去了控制权,2011 年 IETF 甚至专门起草过一份扩展协议试图解决这件事,但因为没法保证对方服务器也支持同一套扩展,这份草案从未成为标准。国内邮箱常被夸的"撤回"功能,本质上也是同一个套路的变种:腾讯企业邮箱官方文档写得很明白,撤回只对发往腾讯企业邮箱和 QQ 邮箱的信件生效,发往网易、Gmail 等其他服务商的邮件一样撤不回来——跟 Outlook/Exchange 的撤回只在同一个组织内部生效是同一个逻辑,都是靠"收发双方恰好被同一家公司控制着"这个条件作弊,而不是谁在协议层面真正解决了这个问题。换句话说,我这次事故里犯的错,换成任何一个国家、任何一套邮件系统,大概率一样收不回来。

心理负担是一回事,这套组合拳在扩展性上还有第二层问题:它撑得住一个域名,撑不住我手上这一把域名。真到了想在第二个品牌域名上复刻同一套配置的时候,就卡住了——Resend 的免费额度和单域名限制,跟十年前的 Mailgun 是一个模子刻出来的问题:我域名多,这类工具是按"一个项目一个域名"设计的,根本不是给"一个人手上一把域名"这种用法准备的。

AWS:两个容易被混着看的产品

调研过程里也认真看过 AWS。SES 是给开发者用的发送/接收基础设施,没有邮箱界面,要自己拼装存储和前端——验证域名、sandbox 出站审批、IAM、跨区域、账单条目又多又碎,个人用户实在不想把业余时间耗在读计费明细上。WorkMail 则完全是另一回事,它是全托管邮箱,按人头 $4/月一个用户,原生支持 IMAP 客户端,跟 Google Workspace 是同一类选择。WorkMail 我在公司的产品上真正用过——贵、配置复杂,这两点是切身体会。配置复杂里有一部分来自域名:WorkMail 对 Route 53 之外的 DNS 服务商并不是彻底封死,添加域名的时候系统会把该配的 MX、CNAME、SPF/DKIM/DMARC 记录列出来,让你手动去自己的 DNS 那边一条条粘贴、等生效、回来验证——只是丢了 Route 53 那个一键批量写入的自动化,对我这种域名托管在 Cloudflare、手上一把域名的人来说,这份手工成本本身就是一种劝退。更关键的是,这条路现在已经没有讨论的意义了:AWS 官方已经宣布终止 WorkMail,2026 年 4 月底起不再接受新用户注册,2027 年 3 月底彻底停服,官方建议现有用户迁移到第三方方案——WorkMail 的问题已经不只是"按人头收费跟自建路线无关",而是这条路本身快走到头了。

把十一年来这一圈选型收进一张表:

方案 时间 解决的问题 主要卡点
SendGrid / Mailgun 2015 发信(EDM 场景) 只发不收,非完整邮箱;Mailgun 后来还遇到域名数上限
网易企业邮箱 2015 自定义域名收发 免费额度小,用起来别扭,很快出局
腾讯企业邮箱 2015 起 自定义域名收发 早期域名数大方,后来大幅收紧;叠加隐私顾虑后弃用
群晖 DS918P + 独享 IP 2018 尝试完整自建收发 消费级 IP 段不被信任,发信总进垃圾箱
Linode / Vultr / Lightsail + Postfix 多年间陆续试过 尝试完整自建收发 25 端口默认封锁,解封申请常被拒
Zoho 免费版 2021 开箱即用的收发 免费版无 SMTP/IMAP,绑定官方 App
Cloudflare Email Routing 2022 收件转发到 Gmail 只解决收件,发件仍靠 Gmail 本身
Resend(挂 Gmail Send As) 2024 发件(自定义域名 From) 单域名限制,Gmail 仍是真正的邮箱
AWS SES 调研过 完整自建的基础设施 配置链条长,sandbox 审批
AWS WorkMail 公司产品用过 全托管邮箱 贵、配置复杂;已宣布 2027 年 3 月停服

看完这张表,结论已经很清楚:十一年时间里,"收"和"发"这两块拼图从来没有真正同时握在自己手里过。2022 到 2024 年这套 Cloudflare Routing + Resend + Gmail 的组合,其实已经是我能拼出来的最好方案了——但拼出来的仍然是"披着自定义域名外壳的 Gmail",Gmail 依然是那个真正的邮箱应用,我掌控的只是收发两条链路的入口和出口,中间的存储、界面、数据完全不在自己手里。

2026 年:真正的最后一块拼图

Cloudflare Email Routing 我从 2022 年就在用,一直很稳定;真正让"完全自建"这件事变得可能的,是 2026 年 Cloudflare 上线的 Email Service——通过 Workers 的 send_email binding、REST API,或者一个认证过的 SMTP 提交端点,都能直接发信。这不是又一个"补发件"的代发工具,而是让 Routing(收)和 Send(发)第一次同时握在同一个账号、同一套 DNS、同一个技术栈里——不再需要 Gmail 当中间那个"真正的邮箱",也不再需要 Resend 这类第三方来补发信这一块。

这里有一个很容易踩的坑,值得单独拎出来说:在给发信域名完成"onboard"这一步之前,账号只能发信给已经验证过的目的地址。完成 onboard 之后(Cloudflare 会自动帮你在 cf-bounce 这个子域下配好 SPF/DKIM/DMARC/MX 记录),才能立刻对任意收件人发信。我在第一次测试的时候就卡在这一步——邮件死活发不出去,一度以为是代码哪里写错了,后来才发现是账号还停留在"发信沙盒"状态,这一步没走完。

对我这种用法来说,这块拼图意味着:

  • 不用再靠 ISP 给的独享 IP + DDNS 这种土办法去凑一个"公网身份"
  • 不用再单独接一个 Mailgun 或者 Resend,只为了补上发信这一块
  • 不用绑 Zoho 的官方 App,也不用把 Gmail 当成事实上的邮箱主体
  • 可以在自己的域名下,用自己写的 Web UI 收发,正文和附件都落在自己的 R2 里

于是这个项目的起点,就是一行写在笔记里的话:研究一下怎么用 Cloudflare 来收发邮件,构建一个真正属于自己的邮件系统。

架构:Routing 收,Send 发,Workers/R2/D1 存

想清楚要什么之后,架构反而是最直白的一部分:

外部发件 ──MX──► Email Routing ──► Worker email() handler
                                        ├─ D1(元数据)
                                        └─ R2(正文/附件)

Web UI ──HTTPS──► Access 鉴权 ──► Hono API ──► send_email binding ──► 外部

收件那条线:MX 记录指向 Cloudflare,触发 Worker 的 email() handler,用 postal-mime 把原始邮件解析出来,正文和附件存进 R2,from/to/subject/thread_id 这些元数据写进 D1。发件那条线:Web UI 提交表单,走 Cloudflare Access 做鉴权,Hono 写的 API 层处理业务逻辑,最后调用 send_email binding 把信发出去。

这套架构最终没有落在 MailPlus、Zoho、AWS 或者「Resend + Gmail」上,核心原因收进一张表也很清楚:

路线 自有域名 能收能发 真正的邮箱主体 运维负担
群晖 MailPlus + 独享 IP ⚠️ 发信总进垃圾箱 自己
Zoho 免费 ✅(额度紧) Zoho
Cloudflare Routing + Resend + Gmail Gmail
AWS SES ⚠️ 要自己拼装 自己 很高
CF Routing + Send + 自建 UI ✅(需完成域名 onboard) 自己 低(Serverless)

Cloudflare 这条路径唯一算得上代价的是供应商锁定——这一点我不打算回避。每套建立在单一供应商之上的系统,我都习惯提前想好一份"逃生计划",Cloudflare 也不例外,这部分留到后面聊生产环境教训的时候展开讲。眼下这个项目还完全在免费额度的舒适区里,这份逃生计划还没有被真正的"超额"或者"断供"场景验证过——这是它现在的边界。

技术选型收个尾:

选型 理由
后端 Workers + Hono + TypeScript 跟 email handler 同进程,共享 D1/R2
前端 React + Vite + TanStack Router/Query 纯 SPA,类 Gmail 三栏布局
存储 D1 + R2 列表查询走 SQLite,大对象丢对象存储
认证 Cloudflare Access (Zero Trust) 免自建登录,JWT 校验
仓库 pnpm monorepo + shared types 前后端类型一致

M0:让第一封信跑通

基础设施三件套:配好 DNS 的 MX/SPF/DKIM/DMARC,Email Routing 设一条 catch-all 规则转发给 Worker,Email Service 完成发信域名的 onboard(前面提到的那个坑,别忘了这一步)。

收件路径:postal-mime 把 raw email 解析成结构化数据;R2 的 key 按 mail/{addressId}/{messageId}/raw.eml | body.html | attachments/... 这样组织;D1 存 from、to、subject、snippet、thread_id、folder、flags 这些字段方便查询。

发件路径:mimetext 构建 MIME 消息,附件走 base64,In-Reply-To/References 头维持住线程关系,发送状态写进 messages.send_status 字段方便排查。

第一版 Web UI 很朴素:收件箱列表加详情页(HTML 内容扔进 iframe sandbox 渲染),写信界面带 From 选择、抄送密送、附件上传,路由用 TanStack Router 管理 /inbox/:messageId 这类地址加上搜索参数。

2026 年 6 月 17 日,M0 加 M1 一起上线到了 mail.bobonaicha.com——从 2015 年第一次在 Mailgun 上加域名,到群晖、云主机、Zoho、Cloudflare Routing、Resend 这一路辗转,再到现在把 Gmail 也甩开,前后整整十一年。路径换了一茬又一茬,但最初那个"我想要一个自己域名下的邮箱"的念头,一直没变。


这一篇先讲到这里——收发两条腿总算都立起来了,而且这次是真正握在自己手里,不再是披着外壳的 Gmail。多域名、多用户权限这些"企业邮箱"该有的能力是怎么长出来的,隐私加固走了哪些弯路,还有 IMAP/SMTP 客户端支持这个我一直没绕开的心结,留到下一篇再聊。


参考资料

文中涉及的第三方政策、产品限制和历史沿革,写作前逐一核实过官方文档或权威信源,附在这里方便读者自行查证,也算是把"只写可核实事实"这个约束落到实处。

云主机端口封锁政策

云厂商产品历史

Mailgun 域名限制沿革

Mailgun 收件能力边界

Zoho Mail 免费版协议访问限制

AWS SES 与 WorkMail 的产品区别

邮件撤回功能的普遍局限

群晖 MailPlus、腾讯/网易企业邮箱、Resend 单域名限制这几段,是本人的第一手使用经历,没有对应的可供核查的外部文献,按大纲里"只写实际经历"的约束如实记述,不额外注水式引用。