Mailpit 与 MailHog 邮件捕获工具手册
注册接口成功了,验证码邮件却无处可查
开发环境里最难受的邮件故障通常长这样:注册接口返回成功,应用日志也写着“邮件已发送”,测试人员却收不到验证码。直接连真实 SMTP 会引入账号、网络、限流和误发风险;把邮件内容打印到日志又会泄露验证码,而且无法验证 MIME、HTML、附件和收件人。
邮件捕获工具把这段链路拆成两个可观察动作:应用仍按 SMTP 协议提交邮件,捕获服务接收后不向公网投递,而是把原始邮件和摘要保存起来,再通过 Web UI 与 HTTP API 展示。人工可以检查模板,自动化测试可以查询并断言邮件。它证明的是“应用已经生成并提交了什么”,不能证明真实邮件服务的信誉、退信、DKIM、SPF 或最终送达。
新项目宜锁定 Mailpit v1.30.4。它是 MIT 许可的活跃项目,提供 Windows、Linux、macOS 静态二进制以及 386、amd64、arm64 容器镜像;默认监听 SMTP 1025 和 Web UI/API 8025。安装入口和版本发布页分别用于确认平台包与升级风险。v1.30.4 含面向公开 SMTP 实例的安全修复,共享环境不应停在更早的 v1.30.x 补丁。
MailHog v1.0.1 同样采用 MIT 许可,默认端口也为 1025/8025,但其发布线和官方 mailhog/mailhog 镜像长期没有更新,官方镜像标签仅列出 linux/amd64。Mailpit 项目也明确把 MailHog 描述为不再维护。历史项目可以把 MailHog 作为锁版兼容依赖继续运行;新基线选择 Mailpit,可以减少架构适配、旧依赖和安全维护成本。不要把未归档的仓库状态误解为仍有稳定发布与安全响应承诺。
先让一个隔离邮箱跑起来
个人临时调试可以下载单文件二进制:macOS 可用 brew install mailpit,Windows、Linux 和 macOS 也可从 Mailpit Releases 下载对应压缩包。启动前执行 mailpit --version,确认实际二进制与项目锁定版本一致:
mailpit --listen 127.0.0.1:8025 --smtp 127.0.0.1:1025项目联调更适合 Compose。下面的配置把宿主机入口限制在回环地址,并显式启用持久化、数量上限、时间上限和单封大小上限:
services:
mailpit:
image: axllent/mailpit:v1.30.4
ports:
- "127.0.0.1:1025:1025"
- "127.0.0.1:8025:8025"
environment:
MP_DATABASE: /data/mailpit.db
MP_MAX_MESSAGES: 500
MP_MAX_AGE: 3d
MP_MAX_MESSAGE_SIZE: 10
MP_LABEL: your-project-local
MP_BLOCK_REMOTE_CSS_AND_FONTS: "true"
volumes:
- mailpit-data:/data
volumes:
mailpit-data:执行启动和运行信息检查:
docker compose up -d mailpit
docker compose ps mailpit
curl -fsS http://127.0.0.1:8025/api/v1/info | jq '{Version,Messages,Database,DatabaseSize}'预期看到 Version 为锁定版本、Messages 为当前邮件数、Database 指向 /data/mailpit.db。若 curl 返回连接拒绝,先看 docker compose logs mailpit,再检查 8025 是否已被占用;若容器持续重启,优先检查 volume 权限和环境变量格式。
这些字段会直接改变运行结果:
| 字段 | 改变的行为 | 错配后的证据 |
|---|---|---|
MP_DATABASE | 指定 SQLite 文件后,重启仍可读取邮件;不指定时使用退出即删除的临时库 | 重启后列表清空,/api/v1/info 显示临时数据库 |
MP_MAX_MESSAGES | 周期性删除最旧邮件;默认保留最近 500 封,0 表示关闭数量清理 | CI 命中旧邮件,数据库持续膨胀 |
MP_MAX_AGE | 按 h 或 d 删除超龄邮件,可与数量上限同时使用 | 共享邮箱保留敏感内容超过约定时间 |
MP_MAX_MESSAGE_SIZE | 限制 SMTP 与 Send API 接受的单封邮件大小,单位 MB | 大附件被 SMTP/API 拒绝,日志中的 rejected 计数增加 |
MP_UI_AUTH_FILE | 同时保护 Web UI 和普通 API | 浏览器弹出 Basic Auth;未带凭证的 API 返回 401 |
MP_SMTP_AUTH_FILE | 要求 SMTP 客户端认证 | 客户端出现 535 一类认证失败,邮箱没有新增消息 |
MP_SMTP_REQUIRE_STARTTLS | SMTP 会话升级 TLS 后才允许继续 | 不支持 STARTTLS 的测试客户端在握手阶段失败 |
MP_SMTP_ALLOWED_RECIPIENTS | 用正则限制可捕获的收件人 | 不合规收件人被拒绝或按配置忽略 |
Mailpit runtime options 会随发布线变化。升级时应对照锁定版本检查参数,而不是把最新文档中的新字段直接复制给旧镜像。
正向实验:SMTP 提交、API 查询、定向清理
先创建一封不含真实身份数据的 RFC 5322 邮件:
cat > /tmp/mailpit-positive.eml <<'MAIL'
From: no-reply@example.test
To: alice@example.test
Subject: registration verify T0
Content-Type: text/plain; charset=UTF-8
verification-code=TEST-ONLY-123456
MAIL
curl --silent --show-error \
--url smtp://127.0.0.1:1025 \
--mail-from no-reply@example.test \
--mail-rcpt alice@example.test \
--upload-file /tmp/mailpit-positive.eml成功的 SMTP 会话通常不产生正文输出。随后按收件人和主题查询,不要把“列表第一封”当成测试目标:
curl -fsS --get http://127.0.0.1:8025/api/v1/search \
--data-urlencode 'query=to:alice@example.test subject:"registration verify T0"' \
| jq '{total, messages: [.messages[] | {ID,Subject,To}]}'
curl -fsS http://127.0.0.1:8025/view/latest.txt预期搜索结果只包含本次测试邮件,正文包含 TEST-ONLY-123456。/view/latest.txt 适合人手快速确认;并发测试应从搜索结果取 ID,再请求 /api/v1/message/<ID> 或 /view/<ID>.txt,否则另一个测试刚写入的邮件可能成为“latest”。Mailpit 的 API v1 可在运行实例的 /api/v1/ 查看交互式规范。
实验后按查询条件删除,而不是清空共享实例:
curl -fsS -X DELETE --get http://127.0.0.1:8025/api/v1/search \
--data-urlencode 'query=to:alice@example.test subject:"registration verify T0"'
rm -f /tmp/mailpit-positive.eml再次搜索应返回 total: 0。这一步同时验证测试隔离和清理权限。
反向实验:把 SMTP 失败变成可见证据
“应用捕获异常”不能证明重试与降级逻辑正确。Mailpit 的 Chaos 能稳定返回 SMTP 错误。把下面两个环境变量临时加入 Compose 后重建容器:
environment:
MP_ENABLE_CHAOS: "true"
MP_CHAOS_TRIGGERS: "Sender:451:100"重复上一条 SMTP curl。预期客户端收到 451 Chaos sender error 并以非零状态退出,邮箱搜索结果不增加。应用侧应留下“可重试 SMTP 失败”的结构化事件,并按自己的重试预算处理;若接口仍报告永久成功且没有补偿任务,故障已从邮件工具定位到业务发送策略。
实验结束必须移除 Chaos 配置并重建:
docker compose up -d --force-recreate mailpit
curl -fsS http://127.0.0.1:8025/api/v1/chaos | jq .未启用 Chaos 时,接口可能返回功能未启用错误;这正是配置已退出的证据。若要验证“只允许测试域名”,可改用 MP_SMTP_ALLOWED_RECIPIENTS='@example\.test$',向 user@example.com 发送并确认 SMTP 被拒绝、邮箱未新增消息。两类反例分别覆盖上游故障与防误发约束。
接进项目和自动化测试
应用与 Mailpit 同在 Compose 网络时,SMTP 主机是服务名 mailpit;应用运行在宿主机时才使用 127.0.0.1:
spring:
mail:
host: ${MAIL_HOST:mailpit}
port: ${MAIL_PORT:1025}
username: ${MAIL_USERNAME:}
password: ${MAIL_PASSWORD:}
properties:
mail.smtp.auth: false
mail.smtp.starttls.enable: falseexport const mailConfig = {
host: process.env.MAIL_HOST ?? "mailpit",
port: Number(process.env.MAIL_PORT ?? 1025),
secure: false,
};开发 profile 可以默认指向捕获服务,生产 profile 必须要求显式注入 SMTP 地址和凭证。启动日志应输出 profile、SMTP host、port、TLS 与 auth 开关,但不能输出用户名、密码或邮件正文。容器里的 localhost:1025 指向应用容器自己;遇到 ECONNREFUSED 或超时时,应在应用容器内执行 nc -vz mailpit 1025,而不是反复重启 Mailpit。
可靠的集成测试使用唯一关联值:
测试开始前生成测试专用收件人或主题标识,例如 case-<RUN_ID>@example.test。调用真实业务入口,等待应用完成异步投递。轮询 /api/v1/search,总超时受测试预算约束;不要固定休眠若干秒。
读取命中邮件的结构化字段、文本或 HTML,仅断言收件人、主题、关键链接参数和必要文案。按关联值删除本次邮件,即使断言失败也在 teardown 中清理。
CI 应为每个 job 启动独立 Mailpit 容器,使用无状态临时数据库,job 结束后删除容器。共享收件箱会让并发 job 争抢“最新邮件”,也会把失败用例留下的验证码带到后续任务。
认证、TLS 与隐私不是一个开关
Mailpit 默认 SMTP 无认证、无加密,UI/API 也无认证,这适合绑定在个人回环地址的临时实例。共享环境至少要把 UI/API 放在受控网络中,并启用 Basic Auth 或由统一入口完成身份认证。密码文件可以包含多个用户名,但这些用户名访问的是同一个邮箱,并不形成用户级数据隔离。
developer:$2y$<bcrypt-hash>environment:
MP_UI_AUTH_FILE: /run/secrets/mailpit-ui-auth
volumes:
- ./local-secrets/mailpit-ui-auth:/run/secrets/mailpit-ui-auth:ro密码文件变化后需要重启 Mailpit。不要把明文 MP_UI_AUTH、SMTP 密码或 bcrypt 文件提交进仓库;样例只保留路径。启用 UI 认证后,API 请求也必须认证。curl -u '<user>:<password>' 会把密码放进 shell 历史或进程参数,不适合共享开发机;回环地址上的本地实验可把临时凭证写入权限为 0600 的 netrc 文件,使用后立即删除,共享环境还必须叠加 HTTPS:
umask 077
read -rsp 'Mailpit API password: ' MAILPIT_API_PASSWORD; printf '\n'
printf 'machine 127.0.0.1 login %s password %s\n' \
'developer' "$MAILPIT_API_PASSWORD" > .mailpit.netrc
unset MAILPIT_API_PASSWORD
curl --netrc-file .mailpit.netrc -fsS \
http://127.0.0.1:8025/api/v1/info | jq .
rm -f .mailpit.netrcSend API 可以使用 MP_SEND_API_AUTH_FILE 配置独立凭证,但它只保护 /api/v1/send,不会自动隔离普通查询 API,也不会形成独立邮箱。
SMTP 认证通过 MP_SMTP_AUTH_FILE 配置。Mailpit 默认要求 PLAIN/LOGIN 认证运行在 STARTTLS 或 TLS 上;MP_SMTP_AUTH_ALLOW_INSECURE=true 是本机兼容开关,不应带进共享环境。UI/API HTTPS 使用 MP_UI_TLS_CERT 与 MP_UI_TLS_KEY,SMTP STARTTLS 使用独立的 MP_SMTP_TLS_CERT 与 MP_SMTP_TLS_KEY。证书私钥与密码一样进入 secret 管理,不进入 Compose 文件。
共享测试环境不能只“挂上证书”,还要强制协议并验证客户端确实校验证书。下面假设证书 SAN 包含 mailpit.local,私钥文件仅对容器运行身份可读:
environment:
MP_UI_TLS_CERT: /run/tls/mailpit.crt
MP_UI_TLS_KEY: /run/tls/mailpit.key
MP_SMTP_TLS_CERT: /run/tls/mailpit.crt
MP_SMTP_TLS_KEY: /run/tls/mailpit.key
MP_SMTP_REQUIRE_STARTTLS: "true"
volumes:
- ./local-secrets/tls:/run/tls:ro# SMTP 端应公布 STARTTLS,证书链和主机名都必须通过。
openssl s_client -starttls smtp \
-connect 127.0.0.1:1025 \
-servername mailpit.local \
-verify_hostname mailpit.local \
-CAfile ./local-secrets/tls/ca.crt </dev/null
# UI/API 使用另一条 HTTPS 会话验证,不能拿 SMTP 成功代替。
curl --resolve mailpit.local:8025:127.0.0.1 \
--cacert ./local-secrets/tls/ca.crt \
-fsS https://mailpit.local:8025/api/v1/info | jq .正向证据是 Verify return code: 0 (ok) 与 API 200;若同时启用了 UI Auth,HTTPS 请求还要加上前面创建的 --netrc-file。反向实验去掉 -CAfile 或把 -verify_hostname 改成未写入 SAN 的域名,连接应因未知 CA 或主机名不匹配失败;若加上 -k、--insecure 后才“成功”,证明客户端仍绕过了身份校验,不能作为 TLS 验收结果。MP_SMTP_REQUIRE_TLS=true 表示从连接开始就使用隐式 TLS,并会关闭同端口 STARTTLS;不要同时把两种模式当成可用入口。
邮件 HTML 还存在浏览器侧隐私风险。Mailpit 会阻止脚本和 iframe,但远程图片仍可加载,追踪像素也不会自动屏蔽;MP_BLOCK_REMOTE_CSS_AND_FONTS=true 只阻止远程 CSS 和字体。对未知邮件,使用隔离网络、浏览器拦截策略和假数据,不要把“UI 有 CSP”理解成邮件内容完全无外连。
存储机制决定容量与故障形态
SMTP 接收完成后,Mailpit 把邮件摘要和压缩后的原始邮件写入 SQLite;UI 与 API 从同一数据库读取。未设置 MP_DATABASE 时,临时数据库随进程退出删除。显式文件路径使邮件跨重启保留,也把敏感信息、磁盘空间和备份责任一并保留下来。
本地 SQLite 默认启用 WAL,适合本机磁盘。NFS、Samba 等网络文件系统可能导致锁或 WAL 异常;存储文档 要求这类场景评估 MP_DISABLE_WAL=true,Samba/CIFS 还涉及挂载锁选项。把 SQLite 文件随意放到共享盘不是高可用设计。多实例确有共享存储需求时,Mailpit 支持 rqlite 与 MP_TENANT_ID 表前缀隔离,但这会引入外部数据库、认证和运维成本;多数研发团队用“每项目一个实例”更简单可靠。
删除邮件不代表数据库文件立即缩小。Mailpit 会在空闲且删除比例达到条件时自动 VACUUM;关闭自动 VACUUM 会减少某些时段的 CPU 峰值,却可能造成文件长期膨胀。容量治理同时观察:
mailpit_messages 是否接近保留上限。mailpit_database_size_bytes 是否在清理后回到稳定区间。mailpit_smtp_rejected_total 是否因大小、收件人或 Chaos 异常增加。
搜索延迟是否随历史消息增长。
可通过 MP_ENABLE_PROMETHEUS=true 在受同一 HTTP 认证保护的 /metrics 暴露指标。独立 metrics 监听地址默认没有 TLS 和认证,不能直接暴露到不受控网络。容量阈值应来自测试邮件频率、平均 MIME 大小、附件上限和保留周期,不使用脱离负载的统一数字。
MailHog 兼容迁移时保住行为,不保住旧实现
MailHog 的内存存储适合临时运行,也支持 maildir 和 MongoDB 持久化:MH_STORAGE=maildir 还要提供 MH_MAILDIR_PATH,MH_STORAGE=mongodb 则要管理 MH_MONGO_URI、数据库和 collection。UI/API 可通过 MH_AUTH_FILE 启用 Basic Auth,但这不是用户级邮箱隔离,也不等于 SMTP 入口已受保护。MailHog 没有 Mailpit MP_MAX_MESSAGES 与 MP_MAX_AGE 这样的内置自动保留组合;持久化实例需要外部清理任务或 API 清理,否则容量和敏感邮件会持续累积。它的 API 路径、存储参数和镜像架构与 Mailpit 不同,不能只替换镜像名:
services:
mailhog:
image: mailhog/mailhog:v1.0.1
platform: linux/amd64
ports:
- "127.0.0.1:1025:1025"
- "127.0.0.1:8025:8025"
environment:
MH_STORAGE: memory迁移前先把测试依赖归类为 SMTP 地址、API 查询、删除语义、认证和持久化五项。SMTP host/port 通常可以直接切换;MailHog /api/v2/messages 的响应模型需要改成 Mailpit /api/v1/messages 或 /api/v1/search;MH_STORAGE 需要改成 MP_DATABASE 与保留策略;认证文件格式和启动参数也要重新验证。Apple Silicon 等非 amd64 环境运行旧官方镜像时可能依赖模拟,性能和供应链风险都应作为迁移信号。
回退时保留旧 Compose 文件和锁定镜像,不复用同一个数据卷。Mailpit SQLite、MailHog maildir/MongoDB 不是可互换格式。先停止新实例,切回旧服务与 API 适配层,再用一封假数据邮件验证;不要通过复制数据库文件做“快速回滚”。
停止、清理和退出
个人临时环境可以删除容器但保留邮件:
docker compose down需要彻底删除测试邮件和数据库时,先确认 volume 只属于当前项目,再执行:
docker compose down -v共享实例不使用 down -v,而是按测试关联值调用搜索删除 API。停用工具前还要撤销网关路由、删除 secret、移除 CI service、清理邮件导出与截图,并确认应用开发 profile 不会回退到真实 SMTP。邮件 dump 是明文 .eml 集合,导出目录的保留与销毁要求应高于普通构建日志。
长期使用时,Mailpit 版本、容器 digest、保留上限、最大邮件大小、认证入口和 owner 应进入项目模板。升级先在隔离实例回放正向 SMTP、API 查询、认证失败、大小拒绝和清理实验,再替换共享实例。MailHog 项目则应有明确迁移触发器:无法获得安全修复、目标平台不受官方镜像支持、API 适配成本已低于继续维护旧运行时成本时,停止继续扩展旧实例。
上线前逐项确认
锁定 Mailpit v1.30.4 或经评审的后续补丁,不使用 latest、edge。历史 MailHog 锁定 v1.0.1 与镜像 digest,并记录 linux/amd64 约束和迁移 owner。宿主机与 Compose 网络分别验证了正确 SMTP host,容器内未误用 localhost。
正向邮件能被唯一条件查询,反向 SMTP 错误能留下应用故障证据。UI/API、SMTP、metrics 的监听和认证分别审查,共享环境没有裸露入口。测试邮件不含真实邮箱、验证码、重置链接、手机号、订单或客户正文。
数量、年龄、单封大小、数据库位置、WAL/VACUUM 和监控策略已明确。CI 每个 job 使用独立实例,teardown 会按关联值清理邮件。清理 volume、撤销凭证、迁移和回退路径已演练且不会误删其他项目数据。
