PHPMailer二三事

PHPMailer 是使用人数最多的 PHP 邮件库,然而我却遇到了吊诡的事情。

起因是这样的,ATSAST 在上个月经历了一次服务器迁移,从原来位于香港的服务器迁移到了我们的新加披服务器,这次迁移后,陆续有用户反映找回密码失败。当时想既然大多数用户成功,可能是用户使用的问题。

后来,一位同学表示多次失败,为了证明没有问题,我示范了一次。结果失败了……败了……了……

初步分析,刁老板没有修改这块代码(排除作案嫌疑);代码在本地也正常。翻来覆去,遂联想到服务器迁移,会不会是服务器缺少 so 文件?抱着这种想法,我编译了四五个旧服务器有的链接库,结果问题照旧。

冷静分析片刻,我!想!到!了!

什么都没想到……

但是我发现 PHPMailer 是可以输出 SMTP Error 的具体信息的,于是我打开了 Debug 开关。

$mail->SMTPDebug = true;

结果?什么?报了一个不能连接?

SMTP Error: Could not connect to SMTP host

这不是废话吗……

随后的几个小时,我想到了所有的可能性。最终,夜里 1 点,我在和刁老板说的时候,为了证明没有问题,我们想到了用 telnet 去试试。

结果?学校的 25 端口竟然 no response?那为什么别的服务器没问题?我试了 4G 网,校园网,旧服务器,结果都能连 25 端口,而新服务器连别的 IMAP 服务端口,HTTP 端口都正常??????啥??????

好的,问题找到了,学校邮件服务沙雕……

可是这样子还是不能解决我的问题,如果不能用 SMTP 的话,就不能发送邮件。POP3 和 IMAP 只能接受邮件,这可怎么办?

绝望之余,我想起来之前 25 端口也是学校不公开的,其实学校没有公开任何端口,完全是试了默认 SMTP 端口结果中了。

那么,不如试试 SMTP over SSL?可是尴尬的是,SMTP over SSL 的端口并没有统一标准,各家都是按照自己的心情去设置。

好在学校的邮件服务提供商 CoreMail 是个心大的厂商,说明文档说了默认 SMTP over SSL 端口是 465。那么我们试试吧?

telnet 一下,通了!只不过没有回显,略担心。

好的,现在改成 465,然后协议选择 SSL。可惜,还是发送失败了。

这又是怎么了?明明有 465 啊,就在我以为学校的 465 上只是空端口准备放弃的时候,试了一下本地邮件服务竟然成功跑上 465 了!

那么 465 就是真的有 SMTP 服务了!一定是代码有问题!

遂打开 SMTPDebug,得到如下提示:

SMTP ERROR: Failed to connect to server: (0)

不一样!有没有!两个是不一样的提示!

OK,详细查阅资料,这个提示似乎和 SSL 离不开关系?难道是学校的 SSL 证书验证出了问题?那么关掉就好了吧?

查找错误文本在代码位置,终于锁定出错的位置:

    $this->smtp_conn = @fsockopen($host,    // the host of the server
                                  $port,    // the port to use
                                  $errno,   // error number if any
                                  $errstr,  // error message if any
                                  $tval);   // give up after ? secs

    if(empty($this->smtp_conn)) {
      $this->error = array("error" => "Failed to connect to server",
                           "errno" => $errno,
                           "errstr" => $errstr);
      if($this->do_debug >= 1) {
        echo "SMTP -> ERROR: " . $this->error["error"] . ": $errstr ($errno)" . $this->CRLF . '<br />';
      }
      return false;
    }

$this->smtp_conn 则是通过 fsockopen 去连接指定端口的,如果不通,就直接报错退出了。

现在,只要关闭 fsockopen 的证书验证,问题就有可能解决了吧?

可是失望的是,查阅资料发现 fsockopen 没有办法关闭证书验证……

但是!

不用担心,我们还有 stream_socket_client,这和 fsockopen 功能几乎一样,但是提供了更加丰富的选项,其中就有 SSL 证书相关的设置。

替换后,代码如下:

    $context = stream_context_create([
        'ssl' => [
            'verify_peer' => false,
            'verify_peer_name' => false
        ]
    ]);
    $hostname = $host.":".$port;
    $this->smtp_conn = @$socket = stream_socket_client($hostname, $errno, $errstr, ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);


    // verify we connected properly
    if(empty($this->smtp_conn)) {
      $this->error = array("error" => "Failed to connect to server",
                           "errno" => $errno,
                           "errstr" => $errstr);
      if($this->do_debug >= 1) {
        echo "SMTP -> ERROR: " . $this->error["error"] . ": $errstr ($errno)" . $this->CRLF . '<br />';
      }
      return false;
    }

来跑一下,成功啦!

到这里,就是邮件问题解决的全过程了,我和刁老板调试了一整晚,才解决了这个问题。

最后,学校的邮件服务的 25 端口(而且只有这一个端口)为什么要屏蔽我的服务器…………………………………………

后记

我登录了 GitHub,准备提 issue……

嗯?有人问了 stream_socket_client?

嗯?他的代码是什么?

嗯????

翻开 PHPMailer,最新版本 6.0.6 早已更新了这里的逻辑,现在,PHPMailer 的逻辑是先判断 stream_socket_client 不行再退回 fsockopen。

那么对于最新版本的用户,直接调用的时候传入一个 verify_peer 为 false 等的设置就成。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据