SpringMail 邮件服务 - 第三章. Mailpit 与 GreenMail
SpringMail 邮件服务 - 第三章. Mailpit 与 GreenMail
ProriseSpringMail 邮件服务 - 第三章. Mailpit 与 GreenMail
本章将彻底解决 “调试邮件功能污染真实邮箱” 的痛点,掌握 Mailpit 可视化调试和 GreenMail 自动化测试两大利器。
本章学习路径
- 认知阶段:理解本地邮件服务器的价值
- 工具阶段:掌握 Mailpit 的安装与使用
- 测试阶段:编写 GreenMail 集成测试
- 实战阶段:构建完整的邮件测试体系
3.1. 为什么不应该连接真实邮箱调试
在第二章中,我们每次测试邮件功能都要真实发送到某个邮箱。这在学习阶段没问题,但在实际开发中会带来三个严重问题。
3.1.1. 真实邮箱调试的三大痛点
痛点一:污染收件箱
假设你正在开发一个用户注册功能,需要发送验证码邮件。在调试过程中,你可能需要测试 20 次才能把格式调整满意。这意味着你的测试邮箱会收到 20 封内容几乎相同的邮件。
更糟糕的是,如果你在测试 “批量发送” 功能,一次测试可能发出上百封邮件。这些测试邮件会淹没你的收件箱,甚至可能触发邮箱服务商的反垃圾机制,导致你的账号被临时封禁。
痛点二:无法验证发送失败的场景
在生产环境中,邮件发送可能因为各种原因失败:收件人邮箱不存在、SMTP 服务器拒绝连接、邮件体积超限等。
但在开发阶段,如果你用真实邮箱测试,很难模拟这些失败场景。你无法故意让 QQ 邮箱 “拒绝连接”,也无法创建一个 “不存在的邮箱” 来测试错误处理逻辑。
痛点三:团队协作困难
在团队开发中,每个开发者都需要配置自己的测试邮箱。这带来两个问题:
- 每个人的配置文件都不同,容易误提交到 Git(泄露密码)
- 测试邮件分散在不同人的邮箱中,无法统一查看和对比
3.1.2. 本地邮件服务器的价值
本地邮件服务器(如 Mailpit、GreenMail)可以完美解决上述问题。它们的核心思想是:在你的开发机器上启动一个 “假的 SMTP 服务器”,拦截所有发出的邮件,但不真正投递。
这带来了四个关键优势:
优势一:零污染
所有测试邮件都被拦截在本地,不会发送到真实邮箱。你可以随意测试,发送一万封邮件也不会有任何副作用。
优势二:可视化调试
Mailpit 提供了一个 Web 界面,你可以像使用 Gmail 一样查看所有拦截的邮件,检查 HTML 渲染效果、附件内容、邮件头信息。
优势三:自动化测试
GreenMail 可以嵌入到 JUnit 测试中,让你编写断言验证 “邮件是否发送成功”、“收件人是否正确”、“附件是否完整”。
优势四:团队统一
所有开发者使用相同的本地服务器配置(如 localhost:1025),不需要各自准备测试邮箱,配置文件可以安全地提交到 Git。
3.2. Mailpit:MailHog 的现代继任者
Mailpit 是目前最流行的本地邮件调试工具,它继承了 MailHog 的优秀设计,并修复了后者的诸多问题。
3.2.1. MailHog 停止维护的背景
在 2020 年之前,MailHog 是开发者的首选工具。它用 Go 语言编写,启动速度快,Web 界面简洁。但从 2020 年开始,MailHog 的作者停止了维护,最后一个版本停留在 v1.0.1。
随着时间推移,MailHog 暴露出一些问题:
- 不支持 ARM 架构(无法在 M1/M2 Mac 上原生运行)
- Web 界面老旧,不支持暗黑模式
- 搜索功能弱,无法按发件人、主题过滤
- 没有邮件保留策略,长时间运行会占用大量内存
社区呼吁作者更新,但始终没有回应。于是在 2022 年,一位新西兰开发者 axllent 创建了 Mailpit 项目,重写了 MailHog 的核心功能,并加入了许多现代化特性。
3.2.2. Mailpit 的核心优势
Mailpit 相比 MailHog 有以下改进:
改进一:跨平台支持
原生支持 x86、ARM、Apple Silicon,在 M1/M2 Mac 上运行流畅。
改进二:现代化 UI
- 响应式设计,支持移动端访问
- 暗黑模式
- 实时搜索和过滤
改进三:高级功能
- 支持 SMTP 认证(可以模拟需要密码的 SMTP 服务器)
- 支持 HTTPS(可以测试 SSL 连接)
- 邮件自动过期(避免内存占用过高)
- REST API(可以通过接口查询邮件)
改进四:活跃维护
截至 2026 年,Mailpit 仍在积极更新,每月都有新版本发布。
3.2.3. Docker 一键启动 Mailpit
Mailpit 提供了官方 Docker 镜像,这是最简单的启动方式。
步骤 1:确认 Docker 已安装
在终端执行以下命令,确认 Docker 正常运行:
1 | docker --version |
如果输出类似 Docker version 24.0.7 的信息,说明 Docker 已安装。如果提示命令不存在,请先安装 Docker Desktop。
步骤 2:启动 Mailpit 容器
在终端执行以下命令:
1 | docker run -d --name mailpit -p 8025:8025 -p 1025:1025 axllent/mailpit:latest |
命令解读:
-d:后台运行容器--name mailpit:给容器命名为 mailpit(方便后续管理)-p 8025:8025:映射 Web 界面端口(访问 http://localhost: 8025 查看邮件)-p 1025:1025:映射 SMTP 端口(Spring Boot 连接这个端口发送邮件)axllent/mailpit:latest:使用最新版本的 Mailpit 镜像
执行后,Docker 会自动下载镜像并启动容器。首次运行需要下载约 20MB 的镜像,后续启动只需 1 秒。
步骤 3:验证 Mailpit 是否启动成功
打开浏览器,访问 http://localhost:8025,你应该能看到 Mailpit 的 Web 界面。界面类似邮箱客户端,左侧是邮件列表(目前为空),右侧是邮件详情。
如果无法访问,执行以下命令检查容器状态:
1 | docker ps |
你应该能看到一行包含 mailpit 的记录,状态为 Up。如果状态是 Exited,说明容器启动失败,执行 docker logs mailpit 查看错误日志。
常用管理命令
1 | # 停止 Mailpit |
3.2.4. 修改 Spring Boot 配置连接 Mailpit
Mailpit 启动后,我们需要修改 Spring Boot 的配置,让邮件发送到 Mailpit 而不是真实的 SMTP 服务器。
在 src/main/resources 目录下创建一个新的配置文件 application-dev.yml(开发环境专用配置):
📄 文件:src/main/resources/application-dev.yml
1 | spring: |
配置解读:
host: localhost:连接本地的 Mailpitport: 1025:Mailpit 的 SMTP 端口username和password留空:Mailpit 默认不需要认证auth: false:关闭 SMTP 认证starttls.enable: false:关闭加密(本地调试不需要)
现在修改 application.yml,在文件开头添加:
1 | spring: |
这样 Spring Boot 会自动加载 application-dev.yml 的配置。
为什么要分离配置文件?
这样做有两个好处:
- 环境隔离:开发环境用 Mailpit,生产环境用真实 SMTP,通过
spring.profiles.active切换 - 安全性:
application-dev.yml不包含敏感信息,可以安全提交到 Git
在生产环境部署时,只需要设置环境变量 SPRING_PROFILES_ACTIVE=prod,Spring Boot 就会加载 application-prod.yml 中的真实 SMTP 配置。
3.2.5. Web 界面调试 HTML 邮件与附件
配置完成后,我们来测试一下。运行第二章中的任意测试方法(比如发送订单确认邮件),然后打开 http://localhost:8025。
你会看到邮件列表中出现了一封新邮件。点击它,右侧会显示邮件详情。
功能一:查看 HTML 渲染效果
Mailpit 默认显示 HTML 版本的邮件。你可以看到邮件的实际渲染效果,包括样式、颜色、布局。
点击顶部的 HTML 标签,可以查看原始 HTML 代码。点击 Plain text 标签,可以查看纯文本版本(如果邮件同时包含 HTML 和纯文本)。
功能二:检查邮件头信息
点击 Headers 标签,可以查看完整的邮件头,包括:
From:发件人To:收件人Subject:主题Content-Type:内容类型(如text/html; charset=UTF-8)Message-ID:邮件唯一标识符
这些信息在排查问题时非常有用。比如,如果邮件中文乱码,检查 Content-Type 是否包含 charset=UTF-8。
功能三:下载附件
如果邮件包含附件,Mailpit 会在邮件详情底部显示附件列表。点击附件名称可以直接下载,验证附件内容是否正确。
功能四:搜索和过滤
在左上角的搜索框中,可以按主题、发件人、收件人搜索邮件。比如输入 订单,会筛选出所有主题包含 “订单” 的邮件。
功能五:查看原始邮件
点击右上角的 Raw 按钮,可以查看邮件的原始 MIME 格式。这对于理解邮件的底层结构非常有帮助。
3.3. GreenMail:单元测试的最佳伴侣
Mailpit 解决了 “可视化调试” 的问题,但它无法自动化。每次测试后,你需要手动打开浏览器检查邮件。在持续集成(CI)环境中,这是不可接受的。
GreenMail 填补了这个空白。它是一个嵌入式的邮件服务器,可以在 JUnit 测试中启动,让你编写断言验证邮件发送逻辑。
3.3.1. 为什么需要邮件测试
在传统的单元测试中,我们会 Mock 掉外部依赖(如数据库、HTTP 接口)。但邮件发送是一个特殊的场景。
Mock 方案的局限性
假设我们用 Mockito Mock 掉 JavaMailSender:
1 |
|
这种测试只能验证 “send 方法被调用了”,但无法验证:
- 邮件主题是否正确
- 收件人地址是否正确
- HTML 内容是否渲染正确
- 附件是否完整
- MIME 格式是否符合标准
GreenMail 的价值
GreenMail 提供了一个 “真实的” SMTP 服务器(虽然是内存中的),让我们可以:
- 发送真实的邮件(经过完整的 MIME 编码)
- 通过 API 查询服务器收到的邮件
- 验证邮件的每一个细节(主题、正文、附件、邮件头)
这就像测试 HTTP 接口时使用 MockMvc 而不是 Mock Controller 一样,我们测试的是 “完整的邮件发送流程”,而不是 “某个方法被调用了”。
3.3.2. GreenMail 的核心概念
在引入依赖之前,我们先理解 GreenMail 的三个核心概念。
概念一:ServerSetup(服务器配置)
定义 GreenMail 启动哪些协议服务器(SMTP/IMAP/POP3)以及监听的端口
GreenMail 不仅支持 SMTP(发信),还支持 IMAP 和 POP3(收信)。在测试中,我们通常只需要 SMTP。
GreenMail 提供了预定义的配置:
ServerSetupTest.SMTP:启动 SMTP 服务器,监听 3025 端口ServerSetupTest.SMTPS:启动加密的 SMTP 服务器,监听 3465 端口ServerSetupTest.IMAP:启动 IMAP 服务器,监听 3143 端口ServerSetupTest.ALL:启动所有协议服务器
为什么端口是 3025 而不是标准的 25?因为 1024 以下的端口需要管理员权限,GreenMail 使用 标准端口 + 3000 作为测试端口。
概念二:GreenMailConfiguration(配置对象)
GreenMail 配置,定义用户账号、认证方式、邮件存储策略等高级配置
这个对象控制 GreenMail 的行为,常用配置包括:
withUser(email, username, password):创建测试用户(用于 SMTP 认证)withDisabledAuthentication():关闭认证(简化测试)withMaxMessageSize(bytes):限制邮件大小
概念三:GreenMailExtension(JUnit 5 扩展)
JUnit 5 扩展,自动管理 GreenMail 的生命周期,在测试前启动、测试后清理
这是 GreenMail 与 JUnit 5 的集成点。它负责:
- 在测试方法执行前启动 GreenMail 服务器
- 在测试方法执行后停止服务器并清理邮件
- 提供 API 查询收到的邮件
3.3.3. 引入依赖并理解版本选择
在 pom.xml 中添加 GreenMail 依赖:
1 | <dependency> |
依赖解读:
greenmail-junit5:包含 JUnit 5 扩展,如果你用的是 JUnit 4,需要引入greenmail-junit4version 2.0.1:截至 2026 年的稳定版本,支持 Jakarta Mail(Spring Boot 3 必需)scope test:仅在测试时使用,不会打包到生产环境
版本兼容性警告:GreenMail 1.x 版本使用的是 javax.mail,与 Spring Boot 3 不兼容。必须使用 2.0+ 版本。
3.3.4. 编写第一个测试:逐步构建
我们不直接给出完整代码,而是一步步构建,理解每个部分的作用。
步骤 1:创建测试类并注册扩展
1 | package com.example.demo.service; |
代码解读:
@RegisterExtension:JUnit 5 的扩展注册注解,告诉 JUnit “这个测试类需要使用 GreenMail 扩展”static:扩展必须是静态的,因为它的生命周期由 JUnit 管理,不属于某个测试实例ServerSetupTest.SMTP:只启动 SMTP 服务器(端口 3025)
步骤 2:配置用户认证
1 | import com.icegreen.greenmail.configuration.GreenMailConfiguration; |
withUser 方法的三个参数:
- 邮箱地址:这个用户的邮箱(可以是任意值,GreenMail 不会真正验证)
- 用户名:SMTP 认证时使用的用户名
- 密码:SMTP 认证时使用的密码
为什么需要创建用户?因为 GreenMail 默认开启了 SMTP 认证。如果你的 Spring Boot 配置中设置了 mail.smtp.auth=true,就必须提供匹配的用户名和密码。
步骤 3:配置生命周期策略
1 |
|
withPerMethodLifecycle 控制 GreenMail 的启动时机:
true(推荐):每个测试方法执行前重启 GreenMail,确保测试隔离(邮件不会互相干扰)false:整个测试类只启动一次 GreenMail,所有测试方法共享同一个服务器
什么时候用 false?当你有 100 个测试方法,且它们之间没有依赖关系时,用 false 可以加快测试速度(避免重复启动)。但要注意清理邮件,避免测试互相影响。
步骤 4:配置 Spring Boot 连接 GreenMail
注意: 这里是测试文件夹下的 application.yml,不要搞错
在 src/test/resources/application.yml 中添加:
1 | spring: |
配置解读:
port: 3025:GreenMail 的 SMTP 端口username和password:必须与withUser中的参数匹配auth: true:开启认证(与 GreenMail 的默认行为一致)
步骤 5:修改 MailService 使用配置中的发件人
1 | import org.springframework.beans.factory.annotation.Value; |
为什么要这样改?因为在测试环境中,我们不能使用真实的 QQ 邮箱作为发件人。通过 @Value 注入配置,可以在不同环境使用不同的发件人地址。
步骤 6:编写测试方法
1 | import jakarta.mail.internet.MimeMessage; |
getReceivedMessages() 方法返回 GreenMail 接收到的所有邮件(按接收顺序排列)。每封邮件都是一个标准的 jakarta.mail.internet.MimeMessage 对象,我们可以用 JavaMail API 读取它的任何属性。
3.3.5. GreenMail 核心 API 详解
现在我们已经有了一个可运行的测试,接下来深入学习 GreenMail 提供的 API。
API 一:getReceivedMessages() - 返回所有接收邮箱
1 | MimeMessage[] getReceivedMessages() |
返回所有接收到的邮件。注意:
- 返回的是数组,不是 List
- 邮件按接收顺序排列(最先发送的在 index 0)
- 如果没有邮件,返回空数组(不是 null)
API 二:getReceivedMessagesForDomain(domain) - 返回指定域名邮箱
1 | MimeMessage[] getReceivedMessagesForDomain(String domain) |
只返回发送到指定域名的邮件。比如:
1 | // 只获取发送到 @example.com 的邮件 |
适用场景:测试中发送了多封邮件到不同域名,需要分别验证。
API 三:waitForIncomingEmail(timeout, count) - 等待指定数量的邮件到达
1 | boolean waitForIncomingEmail(long timeout, int count) |
等待指定数量的邮件到达,最多等待 timeout 毫秒。返回值:
true:在超时前收到了 count 封邮件false:超时了,邮件数量不足
示例:
1 | // 发送邮件 |
这个 API 在测试异步发送时非常有用,比 Awaitility 更简洁。
API 四:purgeEmailFromAllMailboxes() - 清空邮箱中的邮件
1 | void purgeEmailFromAllMailboxes() |
清空所有邮箱中的邮件。适用场景:
- 使用
withPerMethodLifecycle(false)时,需要在每个测试方法开始前手动清理 - 一个测试方法中发送了多封邮件,需要在中途清空
示例:
1 |
|
3.3.6. 验证邮件内容的完整示例
现在我们知道了 GreenMail 的 API,来看几个完整的验证场景。
场景一:验证 HTML 邮件
1 |
|
场景二:验证多个收件人
1 |
|
场景三:验证附件
1 |
|
3.3.7. 使用 Awaitility 处理异步发送
在实际项目中,邮件发送通常是异步的。假设我们的 MailService 有一个异步方法:
1 |
|
直接测试会失败,因为测试方法执行完毕时,邮件可能还没发送。
方案一:使用 GreenMail 的 waitForIncomingEmail
1 |
|
方案二:使用 Awaitility(更灵活)
引入依赖:
1 | <dependency> |
编写测试:
1 | import static org.awaitility.Awaitility.await; |
Awaitility 的优势:
- 可以验证复杂的断言(不仅仅是邮件数量)
- 可以自定义轮询间隔
- 失败时会显示详细的错误信息
3.4. Testcontainers 方案(可选)
除了使用 GreenMail 的嵌入式服务器,我们还可以用 Testcontainers 启动一个真实的 Mailpit Docker 容器进行测试。
3.4.1. Testcontainers 的核心价值
在讲具体用法之前,我们先理解:为什么需要 Testcontainers?它解决了什么问题?
问题一:测试环境与开发环境不一致
在 3.3 节中,我们用 GreenMail 做测试,用 Mailpit 做开发。这带来一个隐患:GreenMail 和 Mailpit 的行为可能不完全一致。
比如,GreenMail 对某些非标准的 MIME 格式比较宽容,但 Mailpit 可能会拒绝。这导致 “测试通过,但开发环境有问题”。
问题二:无法测试真实的 Docker 环境
在生产环境中,我们可能会用 Docker 部署邮件服务。如果测试环境用的是嵌入式的 GreenMail,就无法发现 Docker 相关的问题(如网络配置、环境变量)。
Testcontainers 的解决方案
Testcontainers | 测试容器 | 一个 Java 库,可以在测试中启动真实的 Docker 容器,测试完成后自动销毁
Testcontainers 让我们可以在测试中启动一个真实的 Mailpit 容器,这样:
- 测试环境与开发环境完全一致(都是 Mailpit)
- 可以测试 Docker 相关的配置
- 可以测试多个服务的集成(如同时启动 MySQL + Redis + Mailpit)
权衡:什么时候用 Testcontainers
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单元测试(快速反馈) | GreenMail | 启动快(毫秒级),无需 Docker |
| 集成测试(验证完整流程) | Testcontainers | 环境真实,发现更多问题 |
| CI 环境(有 Docker) | Testcontainers | 与生产环境一致 |
| CI 环境(无 Docker) | GreenMail | 无需额外依赖 |
3.4.2. Testcontainers 核心概念
在编写代码之前,我们先理解 Testcontainers 的三个核心概念。
概念一:GenericContainer(通用容器)
GenericContainer | 通用容器 | Testcontainers 提供的基础容器类,可以启动任意 Docker 镜像
它的核心方法:
withExposedPorts(port...):声明容器需要暴露哪些端口getMappedPort(containerPort):获取容器端口映射到宿主机的随机端口getHost():获取容器的 IP 地址(通常是 localhost)
概念二:@Container 注解
1 |
|
这个注解告诉 Testcontainers:“这是一个需要管理的容器”。Testcontainers 会:
- 在测试类加载时启动容器
- 在所有测试方法执行完后停止并删除容器
为什么必须是 static?因为容器的生命周期是 “类级别” 的,不属于某个测试实例。
概念三:@DynamicPropertySource(动态属性源)
1 |
|
这个注解的作用是:在 Spring 容器启动前,动态修改配置属性。
为什么需要它?因为 Testcontainers 启动容器时,会将容器端口映射到宿主机的随机端口(避免端口冲突)。我们无法在 application.yml 中写死端口号,必须在运行时动态获取。
3.4.3. 引入依赖并理解模块划分
Testcontainers 采用模块化设计,核心库只提供容器管理功能,具体的数据库、消息队列等需要单独引入。
最小依赖(通用容器)
1 | <dependency> |
依赖解读:
testcontainers:核心库,提供GenericContainer等基础类junit-jupiter:JUnit 5 集成,提供@Container、@Testcontainers等注解
为什么不需要 Docker 依赖?
Testcontainers 通过 Docker 的 REST API 与 Docker 守护进程通信,不需要在项目中引入 Docker 客户端库。但你的机器上必须安装并运行 Docker。
3.4.4. 启动 Mailpit 容器的完整示例
现在我们逐步构建一个使用 Testcontainers 的测试。
步骤 1:创建测试类并声明容器
1 | import org.testcontainers.containers.GenericContainer; |
代码解读:
@Testcontainers:启用 Testcontainers 支持,JUnit 会扫描@Container注解GenericContainer<>("axllent/mailpit:latest"):使用 Mailpit 的官方镜像withExposedPorts(1025, 8025):暴露 SMTP 端口(1025)和 Web 界面端口(8025)
步骤 2:配置动态端口
1 |
|
registry.add 方法的两个参数:
- 配置键:要覆盖的 Spring 配置属性(如
spring.mail.port) - 值提供者:一个 Lambda 表达式,返回配置值
为什么用 Lambda 而不是直接传值?因为在 @DynamicPropertySource 执行时,容器可能还没完全启动,端口映射还没完成。Lambda 会延迟执行,确保获取到正确的端口。
步骤 3:编写测试方法
1 |
|
步骤 4:通过 REST API 验证邮件
Mailpit 提供了 REST API 查询邮件。我们可以用 Spring 的 RestTemplate 调用:
1 | import org.springframework.web.client.RestTemplate; |
Mailpit API 返回的是 JSON 格式,完整的验证需要解析 JSON。这里为了简化,只检查字符串包含关系。
由于这方案比较繁琐,是比较极端的测试方向,我们就不过多追溯
3.4.5. 三种测试方案的终极对比
现在我们学习了三种测试方案,做一个全面对比:
| 维度 | GreenMail 扩展 | Testcontainers + Mailpit |
|---|---|---|
| 启动速度 | ⭐⭐⭐⭐⭐(毫秒级) | ⭐⭐⭐(5-10 秒) |
| 环境真实性 | ⭐⭐⭐(嵌入式) | ⭐⭐⭐⭐⭐(与开发环境一致) |
| 验证便利性 | ⭐⭐⭐⭐⭐(Java API) | ⭐⭐⭐(需要 REST API) |
| 依赖要求 | 无 | Docker |
| CI 友好度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 适用场景 | 单元测试、快速反馈 | 端到端测试、生产验证 |
推荐策略:
- 日常开发:使用 GreenMail 扩展(快速反馈)
- 预发布验证:使用 Testcontainers + Mailpit(与生产一致)
3.5. 本章小结与测试策略速查
在这一章中,我们掌握了本地邮件调试和自动化测试的完整方案。
核心知识回顾
我们解决了以下问题:
- 为什么不应该用真实邮箱调试(污染收件箱、无法模拟失败、团队协作困难)
- 如何使用 Mailpit 进行可视化调试(Docker 启动、配置切换、Web 界面)
- 如何使用 GreenMail 编写自动化测试(JUnit 扩展、断言验证、异步处理)
- 如何使用 Testcontainers 构建隔离的测试环境
三种测试方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Mailpit | 本地开发调试 | 可视化、实时查看、支持搜索 | 无法自动化 |
| GreenMail | 单元测试、CI 环境 | 嵌入式、快速、可断言 | 不是真实 SMTP 服务器 |
| Testcontainers + Mailpit | 集成测试 | 环境真实、隔离性好 | 启动慢、需要 Docker |








