Mailpit:SMTP 提交之后如何留下可查询证据
注册接口成功了,验证码邮件却无处可查
开发环境里最难受的邮件故障通常长这样:注册接口返回成功,应用日志也写着“邮件已发送”,测试人员却收不到验证码。直接连真实 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,不要把迁移理解成替换镜像名。先盘点 SMTP 地址、API 查询、删除语义、认证和持久化五项:SMTP host/port 通常可以平移,MailHog /api/v2/messages 则要改接 Mailpit /api/v1/messages 或 /api/v1/search;旧 maildir/MongoDB 数据也不能直接当作 Mailpit SQLite 使用。完整的旧系统维持、风险和退出步骤见 MailHog:遗留邮件捕获服务如何安全退出。
停止、清理和退出
个人临时环境可以删除容器但保留邮件:
docker compose down需要彻底删除测试邮件和数据库时,先确认 volume 只属于当前项目,再执行:
docker compose down -v共享实例不使用 down -v,而是按测试关联值调用搜索删除 API。停用工具前还要撤销网关路由、删除 secret、移除 CI service、清理邮件导出与截图,并确认应用开发 profile 不会回退到真实 SMTP。邮件 dump 是明文 .eml 集合,导出目录的保留与销毁要求应高于普通构建日志。
长期使用时,Mailpit 版本、容器 digest、保留上限、最大邮件大小、认证入口和 owner 应进入项目模板。升级先在隔离实例回放正向 SMTP、API 查询、认证失败、大小拒绝和清理实验,再替换共享实例。
上线前逐项确认
锁定 Mailpit v1.30.4 或经评审的后续补丁,不使用 latest、edge。宿主机与 Compose 网络分别验证了正确 SMTP host,容器内未误用 localhost。
正向邮件能被唯一条件查询,反向 SMTP 错误能留下应用故障证据。UI/API、SMTP、metrics 的监听和认证分别审查,共享环境没有裸露入口。测试邮件不含真实邮箱、验证码、重置链接、手机号、订单或客户正文。
数量、年龄、单封大小、数据库位置、WAL/VACUUM 和监控策略已明确。CI 每个 job 使用独立实例,teardown 会按关联值清理邮件。清理 volume、撤销凭证、迁移和回退路径已演练且不会误删其他项目数据。
