Spring JavaMailSender 无法连接 Testcontainers Mailpit,如何解决?
结论
两个容器已经加入同一个 TESTCONTAINERS_NETWORK,被测服务应通过 Mailpit 的网络别名和容器内部的 SMTP 端口连接:
mailpit:1025
为 Mailpit 设置 .withNetworkAliases("mailpit"),再将 JavaMailSender 的主机名改为 "mailpit"。这条连接不需要 Testcontainers.exposeHostPorts(1025) 或 withAccessToHost(true)。
[EOF] 表示客户端读取 SMTP 欢迎消息时,连接已经结束。当前配置应先修正连接地址。仅凭这条异常,还无法确定是哪个服务或转发环节关闭了连接。
为什么当前配置会失败
host.testcontainers.internal 用于让容器访问运行测试进程的宿主机,不会自动指向另一个 Testcontainers 容器。
这段代码暴露的是宿主机上的 1025 端口:
Testcontainers.exposeHostPorts(1025);
它不会把 Mailpit 容器内部的 1025 端口绑定到宿主机的同名端口。Testcontainers 通常会将容器端口映射到宿主机的随机可用端口,因此,即使 Mailpit 已暴露 SMTP 端口,也不能认为宿主机的 1025 就是 Mailpit。
当前配置的连接路径相当于:
被测服务容器 → 宿主机:1025
这条路径没有明确连接到 Mailpit。如果宿主机的目标端口没有正确的 SMTP 服务,或者转发链路在建立连接后关闭,客户端就可能读到 [EOF]。
SMTP 服务正常接受连接后,会先发送以 220 开头的欢迎消息。Got bad greeting 出现在读取这条消息时,此时尚未进行 SMTP 身份认证,也未发送 STARTTLS 命令。调整用户名、密码或延长超时,都无法修复连接目标错误的问题。
推荐配置:通过共享网络直接连接
1. 为 Mailpit 设置网络别名
在现有的 MailpitContainer 配置中增加网络别名:
@Container
static MailpitContainer mailpitContainer = new MailpitContainer()
.withNetwork(TESTCONTAINERS_NETWORK)
.withNetworkAliases("mailpit");
被测服务保留共享网络配置:
this.withNetwork(TESTCONTAINERS_NETWORK);
这条 SMTP 连接不再需要暴露宿主机端口,可以删除:
Testcontainers.exposeHostPorts(1025);
也可以删除允许访问宿主机的配置:
this.withAccessToHost(true);
如果被测服务还需要访问宿主机上的其他服务,则按那些连接的需要保留相关配置。
2. 修改 JavaMailSender
如果 Mailpit 仍使用容器内的 1025 端口,并允许无认证、无 TLS 的 SMTP 连接,可以这样配置:
@Bean
public JavaMailSenderImpl createJavaMailSenderBean() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("mailpit");
mailSender.setPort(1025);
mailSender.setProtocol("smtp");
mailSender.setJavaMailProperties(createJavaMailProperties());
return mailSender;
}
private Properties createJavaMailProperties() {
Properties properties = new Properties();
properties.setProperty("mail.smtp.auth", "false");
properties.setProperty("mail.smtp.starttls.enable", "false");
properties.setProperty("mail.smtp.ssl.enable", "false");
properties.setProperty("mail.smtp.connectiontimeout", "180000");
properties.setProperty("mail.smtp.timeout", "180000");
return properties;
}
未启用认证时,可以省略用户名和密码设置。如果 Mailpit 配置了认证或 TLS,则按实际配置调整。
使用 setProperty() 代替 Properties.put(),以字符串形式设置属性值,可以避免属性读取方式带来的差异。本例仍应先修正连接地址。
3. 保证 Mailpit 已经就绪
发送邮件时,Mailpit 的 SMTP 服务必须已经开始监听。
如果被测服务在启动阶段就会发送邮件,应在启动服务容器前为它设置 dependsOn(mailpitContainer),并确认 Mailpit 容器的等待策略包含 SMTP 就绪条件。不能仅凭字段声明顺序,就认为两个容器已按业务需要完成启动。
什么时候使用 getHost() 和 getMappedPort()
连接地址取决于发送邮件的代码运行在哪里:
| 发送邮件的位置 | 主机名 | 端口 |
|---|---|---|
| 与 Mailpit 共享网络的容器内 | 网络别名,例如 mailpit | 容器内部 SMTP 端口,例如 1025 |
| 宿主机上的测试 JVM 内 | mailpitContainer.getHost() | mailpitContainer.getMappedPort(1025) |
如果 JavaMailSender 运行在测试 JVM 中,可以在 Mailpit 启动后使用:
mailSender.setHost(mailpitContainer.getHost());
mailSender.setPort(mailpitContainer.getMappedPort(1025));
前提是所用的 MailpitContainer 已暴露 1025 端口。
本例的发送代码运行在被测服务容器内,应优先使用网络别名,不要直接套用宿主机侧的地址和映射端口。
修改后仍出现 EOF,如何排查
从被测服务容器内部检查 mailpit:1025,以验证应用实际使用的网络路径。如果镜像中安装了 nc,可以执行:
nc -v mailpit 1025
正常情况下,应能看到以 220 开头的 SMTP 欢迎消息,具体文本取决于 Mailpit 配置。
如果仍收不到欢迎消息,继续检查:
- 两个容器是否确实连接到同一个 Docker 网络,且已设置
mailpit别名。 - Mailpit 的 SMTP 监听端口是否仍为
1025,监听接口是否允许通过容器网络访问。 - 连接的是否为 SMTP 端口,有没有误用 Web 界面的端口。
- Mailpit 是否启用了隐式 TLS,导致服务端与客户端预期的连接协议不同。
- Mailpit 容器日志中是否有启动失败、退出或连接异常。
必要时,可以临时开启 JavaMail 调试日志:
properties.setProperty("mail.debug", "true");
延长 mail.smtp.connectiontimeout 或 mail.smtp.timeout 只能改变等待时间,无法让错误的端点返回有效的 SMTP 欢迎消息。
备注:内容仅供参考。
