# certificate verify failed：SMTP STARTTLS 发信报错怎么修

> 发邮件时 STARTTLS 之后报 certificate verify failed，原因是邮件服务器证书与主机名不符、自签名、过期或缺中间证书。怎么检查 587 和 465 端口上的证书并修复。

Source: https://ssl.sh/zh/errors/smtp-starttls-certificate-error/
Last updated: 2026-11-10

**这个报错的意思是：邮件客户端用 [STARTTLS](https://ssl.sh/zh/glossary/starttls/) 把 SMTP 连接升级为 TLS 之后，拒绝了服务器的证书。** 邮件客户端的检查和浏览器一样：配置的主机名必须出现在证书里，证书链必须到达受信任的根证书。邮件服务器上这些检查更容易失败，因为默认证书往往是自签名的，主机名也经常是别名。

## 快速修复

多数情况是主机名不在证书里，改用证书上的主机名即可。先针对程序实际使用的主机名和端口，查看服务器出示的证书（也可以用 [STARTTLS 检测工具](https://ssl.sh/zh/tools/starttls-checker/)）：

```bash
# 587（或 25）端口 STARTTLS
openssl s_client -starttls smtp -connect mail.example.com:587 -servername mail.example.com \
  -verify_return_error </dev/null 2>&1 | grep -E "subject=|issuer=|Verify return|verify error"
# 465 端口直接 TLS
openssl s_client -connect mail.example.com:465 -servername mail.example.com </dev/null
```

- **subject 或 SAN 里没有你的主机名：** 改用证书上写的主机名，或者为你的主机名安装证书。
- **验证错误 18/19（自签名）：** 换成公网受信任的证书。
- **验证错误 20/21：** 服务器装上完整证书链。

## 原因

连接用的主机名不在证书里最常见，其次是邮件服务器默认的自签名证书。

### 1. 主机名不匹配

虚拟主机和托管邮箱常常出示服务商服务器名的证书（比如 `server12.hosting.example`），而用户配置的是 `mail.example.com`。

- **确认：** 证书的 SAN 列表里没有 SMTP 设置里的主机名。
- **修复：** SMTP 主机名按证书上的名字填写，或者请服务商签发覆盖你别名的证书。用 QQ 企业邮、阿里企业邮箱、网易企业邮箱等服务时，按服务商文档填写它们官方的 SMTP 服务器地址，不要填自己域名的 CNAME 别名。

### 2. 默认的自签名证书

新装的 Postfix、Exim、Dovecot 通常带一张[自签名](https://ssl.sh/zh/glossary/self-signed-certificate/)的 “snakeoil” 证书。

- **确认：** subject 和 issuer 完全相同。
- **修复：** 为邮件主机名签发证书并配置：

```text
# /etc/postfix/main.cf
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file  = /etc/letsencrypt/live/mail.example.com/privkey.pem
```

Dovecot 写 `ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem` 和 `ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem`。

### 3. 缺中间证书

- **确认：** s_client 只显示一张证书，报错误 20 或 21。
- **修复：** 让服务器使用 `fullchain.pem`，而不是 `cert.pem`，见[缺少中间证书](https://ssl.sh/zh/errors/missing-intermediate-certificate/)。

### 4. 续期了但没重载

Postfix 和 Dovecot 只在启动或重载时读取证书，磁盘上的新文件在此之前不会生效。

- **确认：** 对外出示的证书 `notAfter` 比磁盘上文件的旧。
- **修复：** 给 Certbot 加部署钩子：`--deploy-hook "systemctl reload postfix dovecot"`。

### 5. 应用自己的 CA 库过旧

老版本 PHP、Python、Java 可能缺少较新的根证书，此时只有这个应用报错。

- **修复：** 升级运行环境及其 CA 证书包（系统的 `ca-certificates`、php.ini 里的 `openssl.cafile`、Java 的 `cacerts`）。

## 各环境修法

所有客户端的修法相同：Host 填证书上的主机名，客户端保持校验开启，并使用有效的 CA 证书包。

**PHPMailer / Symfony Mailer（含宝塔上的 PHP 网站）：** 修服务器或改正主机名，Host 填证书上的名字。两者都用 PHP 的 OpenSSL 设置，`openssl.cafile` 必须指向有效的证书包。

**Python smtplib：** 用 `smtplib.SMTP(host, 587)`，再 `starttls(context=ssl.create_default_context())`，默认上下文会校验主机名和证书链。

**Java（Jakarta Mail）：** 设置 `mail.smtp.ssl.checkserveridentity=true`，私有根证书导入 truststore，而不是信任所有主机。

**自建邮件服务器并启用了 DANE 或 MTA-STS：** 更换证书后，在旧密钥下线前先更新 TLSA 记录（可用 [TLSA 生成工具](https://ssl.sh/zh/tools/tlsa-generator/)），否则强制 DANE 的发送方会拒绝投递。

## 不要这样做

关闭验证或退回明文 SMTP，都会让账号密码暴露给网络路径上的任何人。

- **关闭对端验证**（PHP 流选项里 `verify_peer => false`，Java 里 `mail.smtp.ssl.trust=*`）。SMTP 账号密码和邮件内容会被路径上的任何人看到。
- **退回不加密的 SMTP。** 账号密码会以明文传输。
- **保留默认自签名证书，让用户自己点“接受”。** 用户会养成习惯，遇到攻击时也照样点接受。
