启用TLS与服务器身份校验时Jakarta Mail发送邮件失败求助
问题场景
使用Jakarta Mail(依赖angus-mail-2.0.2)在JDK 11/17环境下发送邮件,启用TLS并开启mail.smtp.ssl.checkserveridentity=true时,抛出以下异常:
jakarta.mail.MessagingException: Could not convert socket to TLS; nested exception is: java.io.IOException: Can't verify identity of server: XXXXXXXXXXX.com at org.eclipse.angus.mail@2.0.2/org.eclipse.angus.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2173)
深层异常为模块访问权限问题:
java.lang.IllegalAccessException: class org.eclipse.angus.mail.util.SocketFetcher (in module org.eclipse.angus.mail) cannot access class sun.security.util.HostnameChecker (in module java.base) because module java.base does not export sun.security.util to module org.eclipse.angus.mail
关闭mail.smtp.ssl.checkserveridentity=false时邮件可正常发送,且服务器证书(Let's Encrypt)已在默认信任库中,排除证书信任问题。
根因分析
angus-mail-2.0.2版本的SocketFetcher类直接依赖了JDK内部API sun.security.util.HostnameChecker,而Java 9+的模块系统默认禁止导出内部API给第三方模块,导致权限访问异常。
解决方案
1. 升级Angus Mail到修复版本(推荐)
angus-mail在2.0.3及后续版本中已修复该问题,替换了对JDK内部API的依赖,改用标准公开API实现服务器身份校验。
- 调整Maven/Gradle依赖,将
angus-mail版本从2.0.2升级至2.0.3或更高:
Maven示例:<dependency> <groupId>org.eclipse.angus</groupId> <artifactId>angus-mail</artifactId> <version>2.0.3</version> </dependency> - 保持
jakarta.mail-api(2.1.2)和angus-activation版本的兼容性即可。
2. 临时JVM参数 workaround(不推荐生产环境)
如果暂时无法升级依赖,可通过JVM启动参数允许JDK导出内部API给Angus Mail模块:
--add-exports java.base/sun.security.util=org.eclipse.angus.mail
若为模块化应用,需额外添加:
--add-opens java.base/sun.security.util=org.eclipse.angus.mail
注意:该方案依赖JDK内部API,未来JDK版本可能移除该类,存在兼容性风险。
3. 自定义服务器身份校验逻辑
通过自定义SSLSocketFactory或HostnameVerifier替代默认校验逻辑,避免Angus Mail调用内部API:
修改代码中的Properties配置,添加自定义SocketFactory:
// 自定义SSLSocketFactory实现,使用标准HostnameVerifier properties.setProperty("mail.smtp.ssl.socketFactory.class", "com.yourcompany.mail.CustomTLSSocketFactory");
自定义CustomTLSSocketFactory核心示例:
public class CustomTLSSocketFactory extends SSLSocketFactory { private final SSLSocketFactory delegate; private final HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); public CustomTLSSocketFactory() { this.delegate = (SSLSocketFactory) SSLSocketFactory.getDefault(); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { Socket sslSocket = delegate.createSocket(socket, host, port, autoClose); // 手动校验服务器身份 if (!hostnameVerifier.verify(host, ((SSLSocket) sslSocket).getSession())) { throw new IOException("Server identity verification failed for host: " + host); } return sslSocket; } // 其他SSLSocketFactory抽象方法直接委托给delegate实现 @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public Socket createSocket(String host, int port) throws IOException { return delegate.createSocket(host, port); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return delegate.createSocket(host, port, localHost, localPort); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return delegate.createSocket(host, port); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return delegate.createSocket(address, port, localAddress, localPort); } }
添加该配置后,即使开启mail.smtp.ssl.checkserveridentity=true,也会使用自定义的校验逻辑,绕过Angus Mail的内部API调用。
内容的提问来源于stack exchange,提问作者Peter Müller

