OpenMail:用 Node.js 自建一套能收能发的邮件系统(SMTP/POP3/WebMail 单进程全搞定)
本文目录
想给域名配一个 you@yourdomain.com 的邮箱,很多人第一反应是买企业邮箱或托管到第三方。但如果你手里有一台服务器,又想要完全自托管、数据在自己手里的邮箱,自己跑一套邮件系统其实没有想象中难。最近把一套轻量开源邮件系统整理出来了——OpenMail,本文把它的能力、架构和怎么跑起来讲清楚。
这套系统解决什么
邮件服务器之所以劝退,是因为要同时维护好几个服务:收信的 SMTP、发信的提交服务、客户端收信的 POP3/IMAP、以及一个网页端。OpenMail 的思路是用 Node.js 单进程把它们全部跑起来:
- 📥 SMTP 收信:标准入站服务器(25 端口,MX 角色),接收本地域邮件,拒绝开放中继
- 📤 SMTP 发信:提交服务(587 端口,PLAIN/LOGIN 认证),本地收件人直投、外部收件人进并发队列
- 📬 POP3:RFC 1939 完整实现,Foxmail / Thunderbird 直接收信
- 🌐 WebMail:v1.3.0 全新界面,三栏布局、深浅双主题、快捷键、富文本写信、附件收发
- 🐳 部署:Docker 一键部署,数据落盘,重启不丢邮件
换句话说,一个容器 = 一台能用的邮件服务器,开箱即用。
一条命令跑起来(最快的方式)
镜像在没检测到 DB_HOST 时自动使用内置 SQLite,数据落在 /app/data 卷,拉起来就能用:
docker run -d --name openmail \
-p 15682:15682 -p 25:25 -p 587:587 -p 110:110 \
-v openmail-data:/app/data \
docker.cnb.cool/hanyuxin/mail:latest
# 创建管理员账号(会自动创建域名)
docker exec -it openmail node bin/mailadmin.js adduser admin@localhost -p 123456 --admin
然后打开 http://localhost:15682 走安装向导。WebMail、安装向导和 HTTP API 都由应用原生监听,不需要反向代理。
Windows 本机试用建议换端口(25 常被占用):
SMTP_PORT=2525 SUBMISSION_PORT=2587 POP3_PORT=5110 IMAP_PORT=5143 npm start。
一键部署(MySQL 8,高并发推荐)
如果是要认真跑、还打算批量发信,用 MySQL 版。镜像由 CNB 云原生构建在每次推送 main 时自动构建发布到 CNB 制品库,MySQL 由 compose 自动编排:
mkdir openmail && cd openmail
curl -O https://cnb.cool/hanyuxin/mail/-/raw/main/docker-compose.prod.yml
curl -O https://cnb.cool/hanyuxin/mail/-/raw/main/deploy.sh
chmod +x deploy.sh
./deploy.sh # 交互式:填域名/端口 → 写配置 → 拉镜像 → 启动 → 创建管理员
# 或 ./deploy.sh -y # 全默认值
也可以手动两步:
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec openmail \
node bin/mailadmin.js adduser admin@example.com -p 123456 --admin
部署实操:从制品库拉镜像装起来
镜像由 CNB 云原生构建在每次推送 main 时自动构建、发布到 CNB 制品库,Docker 制品库地址:
docker.cnb.cool/hanyuxin/mail
- 标签:
latest(跟随最新构建)、以及:短提交号(如:f5f516d、:dc6c741、:aeb8bcd、:d654aee),想锁定某次构建就用提交号 tag,升级更可控 - 架构:
linux/amd64,镜像约 69.86 MiB,非常轻 - 生产请优先用
:latest或最新提交号,回滚时切回上一个提交号 tag 即可
下面是一台全新 Ubuntu 服务器上的完整实操(以域名 mail.example.com、管理员 admin@example.com 为例):
第 1 步:准备目录与部署文件
mkdir -p /opt/openmail && cd /opt/openmail
curl -O https://cnb.cool/hanyuxin/mail/-/raw/main/docker-compose.prod.yml
curl -O https://cnb.cool/hanyuxin/mail/-/raw/main/deploy.sh
chmod +x deploy.sh
第 2 步:交互式部署(自动写配置 + 拉镜像 + 启动 + 建管理员)
./deploy.sh
脚本会依次问你:域名、各服务端口、MySQL 密码,然后自动完成——写 .env → 拉 docker.cnb.cool/hanyuxin/mail:latest → docker compose up -d → 创建管理员账号。想全部用默认值直接:
./deploy.sh -y # 等价 ASSUME_YES=1 ./deploy.sh
第 3 步:确认服务与端口
docker compose ps # 应看到 openmail 与 mysql 两个容器 running
docker compose logs --tail 50 openmail # 检查启动日志,确认无报错
# 本机探活(WebMail/安装向导)
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:15682/
第 4 步:登录 WebMail 并配置域名
打开 http://<服务器IP>:15682,用创建的管理员登录。到「管理后台 → 域名管理」添加 example.com,系统会自动生成 DKIM 密钥。
第 5 步:一键写入 DNS 解析
到「管理后台 → DNS 配置 → 云解析自动配置」,填入你的云解析服务商(阿里云 / 腾讯云 DNSPod / 华为云)访问密钥,一键写入 A / MX / SPF / DKIM / DMARC 五条记录,然后点「检测解析」确认全部生效。
第 6 步:验证收发
# 用任意 SMTP 客户端给 admin 发一封(无认证入站,仅限本地域)
node -e "
const nodemailer = require('nodemailer');
nodemailer.createTransport({ host: '127.0.0.1', port: 25 }).sendMail({
from: 'friend@outside.com', to: 'admin@example.com',
subject: '你好', text: '来自外部的问候'
}).then(() => console.log('已投递'));
"
发外部邮箱直接在 WebMail 里写,会进发信队列自动投递;生产环境建议配 RELAY_HOST 走邮箱服务商中继(25 端口常被云厂商封禁)。
第 7 步(可选):配 HTTPS 与 TLS
公网使用建议配正规证书。生成自签名证书先顶着:
docker compose exec openmail node bin/mailadmin.js gencert /app/data/certs
要接 Let's Encrypt 证书,把证书路径填进 TLS_KEY / TLS_CERT 环境变量并重启,即可自动启用 STARTTLS。
让域名真正能收发(DNS 助手)
这是自建邮箱最容易踩的坑:邮件能发出去不等于能进收件箱。要让域名对外正常收发,需要配好 MX / SPF / DKIM / DMARC 四条记录。OpenMail 把这步自动化了:
# 添加域名时自动生成 DKIM 密钥(RSA-2048)
npm run admin -- adddomain example.com
# 输出该域名应配置的全部解析记录
npm run admin -- dns example.com
# 在线检测解析是否已生效(逐条实测 MX/TXT 记录)
npm run admin -- dns example.com --check
# 轮换 DKIM 密钥(旧记录建议保留两周过渡)
npm run admin -- dns example.com --rotate
系统会生成 5 条记录:A(邮件主机)、MX(10 mail 主机)、SPF(v=spf1 a:mail主机 mx ~all)、DKIM(openmail._domainkey)、DMARC(v=DMARC1; p=none; rua=...)。
更省事的是在 Web 管理后台 → DNS 配置 里,把 阿里云 / 腾讯云 DNSPod / 华为云 的访问密钥填进去,一键把 A / MX / SPF / DKIM / DMARC 写入云解析(自动创建托管区、幂等更新、长 TXT 自动分段)。所有出站邮件都会自动附加 DKIM-Signature(rsa-sha256),收方服务器能据此验证发件域真实性。
10 万级批量收发(压测数据)
这套系统针对「群发」做了专门优化——10 万人群发,报文只存 1 份(blob 去重),用连接池 + 多行批量 INSERT + FOR UPDATE SKIP LOCKED 事务认领保证多进程安全。内置压测脚本:
npm run bench -- --count 100000 --mode send # 外发队列吞吐
npm run bench -- --count 100000 --mode ingest # 入站收信吞吐
npm run bench -- --count 100000 --mode both
实测(开发笔记本,MySQL 8 本机):
| 场景 | 结果 |
|---|---|
| 外发批量入队(blob 去重,10 万收件人共享 1 份报文) | ~35,000 封/秒,10 万封 2.8 秒入队 |
| 外发并发投递(32 并发 → 本机 sink) | ~1,440 封/秒,10 万封 69 秒,100% 成功零丢失 |
| 入站 SMTP 收信入库(16 连接 PIPELINING) | ~2,160 封/秒,10 万封 46 秒,100% 入库 |
外发到真实互联网时吞吐取决于对端服务器与网络。建议配
RELAY_HOST走专业中继、按 CPU/带宽调大OUTBOUND_CONCURRENCY、多进程水平扩展(SKIP LOCKED 保证不重复投递)。
HTTP API:给程序用的邮箱
除了网页和邮件客户端,还提供 Token 鉴权的编程接口 /api/v1,方便外部系统集成:
# 创建密钥(绑定 alice 的发件身份,scope: full / send / read)
npm run admin -- apikey add alice@example.com --scope full
# 编程发信
curl -X POST http://127.0.0.1:15682/api/v1/send \
-H "Authorization: Bearer omk_xxx" -H "Content-Type: application/json" \
-d '{"to":"user@example.com","subject":"hi","text":"hello","attachments":[{"filename":"a.txt","contentType":"text/plain","content":"aGVsbG8="}]}'
# 收件列表 / 详情 / 统计
curl -H "Authorization: Bearer omk_xxx" http://127.0.0.1:15682/api/v1/messages?folder=inbox
curl -H "Authorization: Bearer omk_xxx" http://127.0.0.1:15682/api/v1/stats
Token 只在创建时明文返回一次(服务端只存 SHA-256),可随时吊销、立即生效。各端点 scope:/api/v1/send(send)、/api/v1/messages(read)、/api/v1/stats(read)、/api/v1/domains(read)。完整文档在 docs/API.md。
架构:v2 monorepo 分层
┌──────────────────────────────────────┐
外部邮件服务器 ──25──▶ │ protocols/smtp-inbound 收信(防开放中继)│──┐
│ quickParse 快速解析 + 规则引擎 │ │ 分组
└──────────────────────────────────────┘ │ 批量
▼
浏览器 ────15682────▶ │ Express 原生托管 React + HTTP API │ MySQL 8
邮件客户端 ──110────▶ │ protocols/pop3 │ utf8mb4
邮件客户端 ──143────▶ │ protocols/imap(IMAP4rev1 子集) │ 连接池
用户 ──587(认证)────▶ │ protocols/smtp-submission ─ 本地直投 │──┘
│ └─ 发信队列 ─┐ │
└────────────────────────────────┼─────┘
▼
│ queue/outbound 并发 worker 池 │
│ SKIP LOCKED 认领 · blob 去重 · 退避 │
└──────────────┬───────────────────────┘
25/MX 或 RELAY_HOST ─▶ 互联网
后端分四层:protocols/(SMTP 收发/POP3/IMAP)→ services/(mail/dns/rules 业务)→ db/(驱动 + 统一异步 API)→ api/(Express 路由,只编排不写 SQL)。报文以原始 RFC822 存入 MySQL(LONGBLOB),入库只做快速头部解析并缓存纯文本供全文搜索;群发场景报文在 blobs 表存一份、queue 行引用,投递完自动清理孤儿 blob。
生产部署须知(重要)
自建邮件系统,能不能用是一回事,能不能不被拒收、不被打进垃圾箱是另一回事。这几点务必注意:
- DNS:为服务器 IP 配 MX / A 记录,并设置 SPF、DKIM、DMARC、反向解析(PTR)——否则发往外网大概率进垃圾箱或被拒收
- 25 端口:阿里云/腾讯云等默认封禁 25 端口出站,推荐配
RELAY_HOST通过邮箱服务商 SMTP 中继发信 - TLS:公网使用请配正规证书(Let's Encrypt 等),或先用
npm run admin -- gencert生成自签名证书 - MySQL 调优:10 万级批量建议
max_connections >= 300、innodb_buffer_pool_size >= 512M、max_allowed_packet >= 64M(compose 已按此配置) - 反垃圾:公网 MX 建议前置反垃圾网关
- 群发合规:批量发信请遵守反垃圾邮件法规与收件方配额,控制并发避免被对端限流
CLI 管理工具速查
npm run admin -- adduser bob@example.com -p 123456 # 创建用户
npm run admin -- passwd bob@example.com newpass # 改密码
npm run admin -- listusers # 用户列表
npm run admin -- adddomain example.com # 添加域名
npm run admin -- addalias team@example.com bob@example.com # 别名
npm run admin -- send alice@example.com bob@example.com "主题" body.txt # 命令行发信
npm run admin -- bulksend news@example.com rcpt@test.local --count 100000 # 10 万级批量入队
npm run admin -- gencert data/certs # 生成自签名 TLS 证书
npm run admin -- stats # 系统统计
小结
自建邮件服务器在过去是「运维劝退项目」,要同时折腾 MTA、MDA、IMAP、Web 界面。OpenMail 把这套东西收敛成一个 Node.js 进程 + 一个 Docker 容器,MySQL 高并发和 SQLite 零配置两头兼顾,DNS 记录还能自动生成和检测。如果你正想给域名配一个自托管邮箱,或者想搞一个内部邮件系统、批量通知通道,它是个值得试试的起点。
仓库地址:cnb.cool/hanyuxin/mail(README 含完整功能列表;接口细节见 docs/API.md、docs/CLIENTS.md、docs/CONFIG.md)