SMTP 发送邮件不适用于 office365

SMTP send mail is not working for office365

这是一个特殊的问题。目的是通过smtp为office365发送邮件。

我一直能够从本地笔记本电脑发送邮件。

但是当部署在我们的服务器上(在防火墙后面)时,它并没有成功。 注意: smtp.office365.com 的端口 587 可访问并已在服务器上确认。以下是它在我的本地计算机上成功运行的属性。

Properties props = new Properties();
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.connectiontimeout", MAIL_TIMEOUT);
props.put("mail.smtp.timeout", MAIL_TIMEOUT);
props.put("mail.debug", true);
this.session = Session.getInstance(props);
session.setDebug(true);

Transport transport  = session.getTransport();
transport.connect("smtp.office365.com", 587, email, pass);

但在服务器上失败。以下是服务器调试日志:

DEBUG: setDebug: JavaMail version 1.6.2
DEBUG: getProvider() returning javax.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle]
DEBUG SMTP: useEhlo true, useAuth false
DEBUG SMTP: trying to connect to host "smtp.office365.com", port 587, isSSL false
220 PN1PR0101CA0017.outlook.office365.com Microsoft ESMTP MAIL Service ready at Fri, 28 Jun 2019 06:39:41 +0000
DEBUG SMTP: connected to host "smtp.office365.com", port: 587
EHLO appqa
250-PN1PR0101CA0017.outlook.office365.com Hello [182.73.191.100]
250-SIZE 157286400
250-PIPELINING
250-DSN
250-ENHANCEDSTATUSCODES
250-STARTTLS
250-8BITMIME
250-BINARYMIME
250-CHUNKING
250 SMTPUTF8
DEBUG SMTP: Found extension "SIZE", arg "157286400"
DEBUG SMTP: Found extension "PIPELINING", arg ""
DEBUG SMTP: Found extension "DSN", arg ""
DEBUG SMTP: Found extension "ENHANCEDSTATUSCODES", arg ""
DEBUG SMTP: Found extension "STARTTLS", arg ""
DEBUG SMTP: Found extension "8BITMIME", arg ""
DEBUG SMTP: Found extension "BINARYMIME", arg ""
DEBUG SMTP: Found extension "CHUNKING", arg ""
DEBUG SMTP: Found extension "SMTPUTF8", arg ""
STARTTLS
220 2.0.0 SMTP server ready
Exception in thread "main" javax.mail.MessagingException: Could not convert socket to TLS;
  nested exception is:
java.net.SocketTimeoutException: Read timed out
at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2155)
at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:752)
at javax.mail.Service.connect(Service.java:366)
at com.company.app.MailReader.getTransport(MailReader.java:269)
at io.vavr.control.Try.of(Try.java:75)
at com.company.app.MailReader.<init>(MailReader.java:59)
at com.company.services.MailService.getNewMailReader(MailService.java:82)
at com.company.services.MailService.start(MailService.java:46)
at com.company.Main.main(Main.java:34)
Caused by: java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
at sun.security.ssl.InputRecord.readV3Record(InputRecord.java:593)
at sun.security.ssl.InputRecord.read(InputRecord.java:529)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at com.sun.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:626)
at com.sun.mail.util.SocketFetcher.startTLS(SocketFetcher.java:553)
at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2150)
... 8 more

尝试将其添加到您的属性中,应该可以解决问题。

props.getProperties().put("mail.smtp.ssl.trust", "smtp.office365.com");

检查服务器是否与您的本地计算机具有相同的证书集。

来自服务器的 220 响应并不意味着 TLS 会话已经建立,它只是意味着客户端可以开始协商它:

After receiving a 220 response to a STARTTLS command, the client MUST start the TLS negotiation before giving any other SMTP commands. If, after having issued the STARTTLS command, the client finds out that some failure prevents it from actually starting a TLS handshake, then it SHOULD abort the connection. (from RFC 3207)

此时,最有可能的问题是缺少证书。

检查服务器上的 JRE 版本,并将其与本地计算机的版本进行比较。

这是一个与环境相关的问题,因为相同的代码在不同的机器上表现不同。没有全貌,我无法肯定地回答。但我希望为进一步调查提供一些见解。我的分析如下:

  • 首先,我认为这不是 SSL 证书问题,根本原因错误很明确:
Caused by: java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
...
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
...

这意味着套接字已经建立,但是将套接字转换为TLS的握手阶段失败了。如果证书无效,握手后会报错,我们看SocketFetcher.javaclass:

中的代码
    /*
     * Force the handshake to be done now so that we can report any
     * errors (e.g., certificate errors) to the caller of the startTLS
     * method.
     */
    sslsocket.startHandshake();

    /*
     * Check server identity and trust.
     */
    boolean idCheck = PropUtil.getBooleanProperty(props,
                prefix + ".ssl.checkserveridentity", false);
    if (idCheck)
        checkServerIdentity(host, sslsocket);
    if (sf instanceof MailSSLSocketFactory) {
        MailSSLSocketFactory msf = (MailSSLSocketFactory)sf;
        if (!msf.isServerTrusted(host, sslsocket)) {
        throw cleanupAndThrow(sslsocket,
            new IOException("Server is not trusted: " + host));
        }
    }
    }

套接字在此行遇到超时:sslsocket.startHandshake(),这是在证书验证之前。

  • 其次,您已经提到防火墙被禁用,我们可以看到之前的套接字已正确建立,telnet 命令也已正确建立,所以我认为这也不是防火墙问题。

  • 看起来像是协议问题,主要是因为这发生在握手阶段,否则我们应该看到不同的和更明确的错误,例如证书错误、连接超时等。这是一个socketRead超时,表示客户端(你的服务器)正在等待服务器(office365)的一些信息,但是服务器没有响应,就像他们没有在交谈一样。

  • 编译后的代码不是这里的问题,但此过程的某些部分与环境相关:SSLSocketImpl.class class 来自 JRE 而不是来自编译。这是实现协议的确切代码(反编译):

private void performInitialHandshake() throws IOException {
        Object var1 = this.handshakeLock;
        synchronized(this.handshakeLock) {
            if (this.getConnectionState() == 1) {
                this.kickstartHandshake();
                if (this.inrec == null) {
                    this.inrec = new InputRecord();
                    this.inrec.setHandshakeHash(this.input.r.getHandshakeHash());
                    this.inrec.setHelloVersion(this.input.r.getHelloVersion());
                    this.inrec.enableFormatChecks();
                }

                this.readRecord(this.inrec, false);
                this.inrec = null;
            }

        }
    }

以上代码来自JRE_1.8.0_181,您的代码或来自您服务器的代码可能不同。这是检查服务器的 JRE 版本所必需的方法。

  • 使用你一开始提供的相同代码,我可以正确连接到office365

问题是防火墙中的一个特定规则。

删除防火墙中的规则解决了这个问题。无需更改特定代码即可使其正常工作。