本地邮件测试工具 Mailpit 的 v1.32.0,HTML 预览地址 /view/{ID}.html 会去掉 <base> 和 meta refresh,原位置写成注释 <!-- base tag removed -->、<!-- meta refresh removed -->。REST API 里的 HTML 字段仍是发信原文。同日发布说明还修了附件缩略图的 EXIF 死循环,官方写明建议升级。

Mailpit 是 Go 写的开发用 SMTP 捕手,协议 MIT,一个静态二进制就能跑,也提供 Docker 镜像。GitHub 简介写的是 “An email and SMTP testing tool with API for developers”,写稿时仓库星数 10725。独立开发者在本机联调注册信、收据、密码重置时,常把 Spring Boot 的 spring.mail.host 指过来,信落进 Web UI 和 API,不经过真实邮件服务商、也不按封数计费。下面全部绑 127.0.0.1,没有对外发信。

费用上它没有 SaaS 档位:MIT 开源,自己跑二进制或容器,成本只在本机或开发机资源。没有按封数计费,也没有托管套餐表要列。和 Resend、Postmark、SES 事务邮件账单 那篇是不同层:那些是生产发信的钱;Mailpit 解决的是开发期“信有没有发出、HTML 长什么样”。

常见联调路径很短:起 Mailpit,Spring Boot 指到 1025 或自定义端口,业务代码照常调用 JavaMailSender,浏览器打开 Web UI 看信,或在测试里用 REST API 断言主题、收件人、正文。v1.32.0 之后,若测试读的是预览页而不是 API,对 base 和 meta refresh 的断言会和发信原文对不上,这是这次升级要一起改测试的地方。

v1.32.0 和安全说明

发布页置顶原句是:This release includes security fixes, so upgrading is strongly recommended. GitHub 时间戳 2026-10-22T22:04:42Z,北京时间 2026-10-23 06:04。和预览、安全直接相关的条目:

  • 缩略图接口不再用 github.com/rwcarlsen/goexif,改成只读首个 IFD、不跟随指针的内置 EXIF 方向读取。对应 advisory GHSA-379p-g4f4-hq5h(2026-10-22 发布):影响版本写 <= 1.31.6,修复版本 1.32.0;摘要是缩略图接口在解析循环 IFD 链时可能陷入死循环并占满 CPU/内存。
  • 从 /view/{ID}.html 输出去掉 <base> 和 meta refresh。
  • HTTP、SMTP、POP3 各自并发连接上限 1000。

同一版还加了搜索自动完成与历史、点击浏览器通知打开对应邮件。这篇只核验预览剥离,以及 Spring Boot JavaMailSender 能否把信打进本机实例。EXIF 那条以 advisory 文本为准,不做攻击复现。

advisory 的部署背景写得很清楚:Mailpit 是开发测试工具,不设计成对不可信网络或公网暴露,那样做也不被支持。默认会监听所有网卡,且 SMTP 与 UI 默认无认证。实例绑 localhost、加防火墙、或开了 SMTP/UI 认证,就不在它描述的暴露面里。本机演示命令是 --listen 127.0.0.1:8027 --smtp 127.0.0.1:1027。

二进制启动

curl -sL -o mailpit.tgz \
  https://github.com/axllent/mailpit/releases/download/v1.32.0/mailpit-linux-amd64.tar.gz
tar xzf mailpit.tgz
./mailpit version
# ./mailpit v1.32.0 compiled with go1.27.3 on linux/amd64

./mailpit --listen 127.0.0.1:8027 --smtp 127.0.0.1:1027

版本字符串:

$ ./mailpit version
./mailpit v1.32.0 compiled with go1.27.3 on linux/amd64

$ ./v131/mailpit version
./v131/mailpit v1.31.6 compiled with go1.27.2 on linux/amd64

GET /api/v1/info:

{
  "Version": "v1.32.0",
  "LatestVersion": "v1.32.0",
  "Messages": 0,
  "RuntimeStats": {
    "SMTPAccepted": 0,
    "SMTPRejected": 0
  }
}

Spring Boot 4.1.1 发信与校验

依赖用模块化的 spring-boot-mail(运行期 Angus Mail 2.0.5)和 spring-boot-starter-restclient:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-mail</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

配置里关闭 SMTP 认证和 STARTTLS,因为本机 Mailpit 默认不要求:

spring.main.web-application-type=none
spring.mail.host=127.0.0.1
spring.mail.port=1027
spring.mail.properties.mail.smtp.auth=false
spring.mail.properties.mail.smtp.starttls.enable=false
mailpit.http=http://127.0.0.1:8027

发一封故意带 <base href="https://evil.example/"> 和 <meta http-equiv="refresh" content="0;url=https://evil.example/phish"> 的 HTML。若浏览器按 <base> 解析,相对链接会指到外部域名;meta refresh 则会跳走。本地预览若原样渲染,人和自动化都容易看错:

MimeMessage msg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg, true, "UTF-8");
helper.setFrom("app@localhost");
helper.setTo("dev@example.test");
helper.setSubject("Mailpit HTML preview check");
helper.setText(
    """
    <html><head>
    <base href="https://evil.example/">
    <meta http-equiv="refresh" content="0;url=https://evil.example/phish">
    <title>receipt</title>
    </head><body>
    <p>Order <a href="/orders/42">#42</a> paid.</p>
    <img src="/logo.png" alt="logo">
    </body></html>
    """,
    true);
mailSender.send(msg);

同一进程里用 RestClient 先清空邮箱,再读列表、读 API 正文、读预览页:

http.delete().uri("/api/v1/messages").retrieve().toBodilessEntity();
// ... send via JavaMailSender ...

Map<?, ?> list = http.get().uri("/api/v1/messages").retrieve().body(Map.class);
String id = (String) ((List<Map<?, ?>>) list.get("messages")).get(0).get("ID");

String html = String.valueOf(
    http.get().uri("/api/v1/message/{id}", id).retrieve().body(Map.class).get("HTML"));
// contains_base=true, contains_meta_refresh=true

String view = http.get().uri("/view/{id}.html", id)
        .accept(MediaType.TEXT_HTML).retrieve().body(String.class);
// contains_base=false, contains_meta_refresh=false, len=210

实测结果

日志时间是北京时间:

2026-10-23T08:06:49.663+08:00  INFO 1247961 --- [           main] com.example.mpdemo.MpDemoApplication     : [1 send] JavaMailSender -> 127.0.0.1:1027 done
2026-10-23T08:06:49.691+08:00  INFO 1247961 --- [           main] com.example.mpdemo.MpDemoApplication     : [2 list] ID=3JZheFuEboRhxzIOkWj03P From={Name=, Address=app@localhost} Subject=Mailpit HTML preview check
2026-10-23T08:06:49.696+08:00  INFO 1247961 --- [           main] com.example.mpdemo.MpDemoApplication     : [3 api HTML] contains_base=true contains_meta_refresh=true
2026-10-23T08:06:49.699+08:00  INFO 1247961 --- [           main] com.example.mpdemo.MpDemoApplication     : [4 view HTML] contains_base=false contains_meta_refresh=false len=210
2026-10-23T08:06:49.699+08:00  INFO 1247961 --- [           main] com.example.mpdemo.MpDemoApplication     : [4 view snippet]
<html><head>
<!-- base tag removed -->
<!-- meta refresh removed -->
<title>receipt</title>
</head><body>
<p>Order <a href="/orders/42">#42</a> paid.</p>
<img src="/logo.png" alt="logo">
</body></html>

同一封信,GET /api/v1/message/3JZheFuEboRhxzIOkWj03P 的 HTML 字段仍是原文:

<html><head>
<base href="https://evil.example/">
<meta http-equiv="refresh" content="0;url=https://evil.example/phish">
<title>receipt</title>
</head><body>
<p>Order <a href="/orders/42">#42</a> paid.</p>
<img src="/logo.png" alt="logo">
</body></html>

GET /view/3JZheFuEboRhxzIOkWj03P.html 则是剥过的版本。相对路径 /orders/42、/logo.png 还在,只是没有 <base> 改解析起点,也不会被 refresh 带走:

<html><head>
<!-- base tag removed -->
<!-- meta refresh removed -->
<title>receipt</title>
</head><body>
<p>Order <a href="/orders/42">#42</a> paid.</p>
<img src="/logo.png" alt="logo">
</body></html>

二进制里也能搜到同样的注释字符串 <!-- base tag removed --> 和 <!-- meta refresh removed -->,和预览页输出一致,说明不是 Web UI 临时改写,而是服务端在生成 /view/{ID}.html 时写入的。

自动化断言应继续读 API 原文;人在浏览器打开的预览,按 v1.32.0 会少掉这两类标签。两个入口内容不一致,是这版的明确行为。

和 v1.31.6 对照

同一台机器再起官方 v1.31.6(端口 1028/8028),塞进同样结构的 HTML。预览页原样保留 <base> 和 meta refresh:

<html><head>
<base href="https://evil.example/">
<meta http-equiv="refresh" content="0;url=https://evil.example/phish">
</head><body><p>hi <a href="/x">x</a></p></body></html>

预览剥离是 v1.32.0 新行为,不是旧版就有的静默规则。

和 listmonk 那篇的区别

listmonk v7 beta 把事务 SMTP 移出默认池 那篇里,Mailpit 只用来冒充两家 SMTP,观察信落在哪条通道。这篇写 Mailpit 自身:Spring Boot 本地捕信,以及 v1.32.0 对预览 HTML 的处理。结论入口都不同。

参考:Mailpit v1.32.0 发布说明(2026-10-22T22:04:42Z / 北京时间 2026-10-23 06:04),GHSA-379p-g4f4-hq5h(2026-10-22),Mailpit 功能说明,axllent/mailpit。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。