↓ 跳过正文

eM Client 发信正常、收信连不上 QQ 邮箱:993 端口的安全策略配错了

·2694 字·6 分钟
目录

症状:eM Client 10 里的一个 QQ 邮箱账号能正常发信,收信却每次卡满 30 秒,报「在30秒超时时间内没有服务器响应」,紧接着账号掉线。IMAP 明细日志里没有 TLS、证书或认证错误,本机代理内核的日志里也没有任何拨号失败。同一台机器上用 openssl 对 imap.qq.com:993 做隐式 TLS,0.05 秒就收到了 QQ 的 IMAP 问候语。差别在客户端配置:这个账号的 993 端口配的是「强制使用 SSL/TLS」,eM Client 于是在等一句明文问候,而只支持 TLS 的 993 永远不会发。改成「在特殊端口上使用 SSL/TLS(旧式)」后,连接变成常驻。适用环境:Windows 11、eM Client 10.4.5674、本机有 TUN 模式的代理(fake-ip)。同类现象的处理顺序:先把网络是否通确认下来,再查账号库里 993 端口的安全策略配对。

一、症状与日志原文
#

eM Client 可以给单个账号开 IMAP 明细日志,文件在 %APPDATA%\eM Client\Logs\。出问题的那次只有 50 行,关键段落如下:

10:36:39.722|079|   01:   Connecting to imap.qq.com:993...
10:37:09.732|079|   01: --- RECEIVE CANCELLED
10:37:09.732|079|   01: A1 LOGOUT
10:37:09.732|079|   01:   Connection 1 was closed, deliberate = False
10:37:09.733|079|   >>> COMMAND CONNECTION EXCEPTION (retry: True): MailClient.Accounts.ConnectionException: 打开与服务器的连接失败。
10:37:09.733|079|    ---> System.IO.IOException: 在30秒超时时间内没有服务器响应。
10:37:09.733|079|      at MailClient.Imap.Base.Connection.ConnectAsync(Func`2 getStream, CancellationToken cancellationToken)
...
10:37:39.740|079|   AccountBase.GoOfflineAsync : State changed to OFFLINE due BrokenConnection

这份日志里有四条线索:

日志现象 说明
连接后精确 30.00 秒被打断 是客户端自己的读超时到点,不是服务器主动断开
全程没有 TLS、证书、认证相关报错 失败发生在拿到服务器问候语之前,与账号密码无关
超时后紧接一句 A1 LOGOUT 客户端认为连接层已建立,于是按流程写一句注销命令再关闭
第二次重试同样 30 秒超时,随后状态转为 OFFLINE due BrokenConnection 账号会保持离线,直到有人手动让它重新上线

最后一条能解释一个常被提到、又容易被当成怪现象的情况:账号掉线后不会自己重连,表现成「只有启动客户端那一次能收到信」。

二、先把网络排除掉
#

服务跑在 TUN 模式的代理后面(fake-ip,客户端看到的服务器地址是 198.18.x.x),所以第一直觉是代理把这条连接搞坏了。查代理内核日志,事实相反:

level=info msg="[TCP] [fake-ip]:64616(MailClient.exe) --> imap.qq.com:993 match DomainSuffix(imap.qq.com) using DIRECT"

这条连接命中了直连规则,并且没有伴随拨号失败。同一天真有拨号失败时,日志长这样,可以直接对照:

level=warning msg="[TCP] dial DIRECT (match RuleSet/CN-D) ... --> ...:443 error: dial tcp ...: i/o timeout"

第二条证据来自独立探测。在同一条网络路径上,用 openssl 对 993 做隐式 TLS:

Connecting to [fake-ip]
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Peer certificate: C=CN, ST=Guangdong Province, L=Shenzhen, O=Shenzhen Tencent Computer Systems Company Limited, CN=*.mail.qq.com
Verification: OK

握手完成后立刻收到服务器问候语,不需要任何认证动作:

* OK [CAPABILITY IMAP4 IMAP4rev1 ID AUTH=PLAIN AUTH=LOGIN AUTH=XOAUTH2 NAMESPACE] QQMail XMIMAP4Server ready

连续 8 次 openssl 加 5 次 python 的 ssl 模块,全部在 0.05 秒内完成握手和问候;明文 143 端口的问候语同样正常。

这里有一个我自己先踩进去的坑:用 curl 的 telnet 模式探 993 端口,永远只会得到超时,因为 TLS 端口在客户端发起握手之前不会先说话。这类假阴性会把人往「端口被墙」的方向带。判断邮件端口通不通要用 openssl s_client 或语言自带的 TLS 库,不要用裸 TCP 探测。

到这一步可以下判断:从这台机器到 imap.qq.com 的 TCP 和 TLS 都通,服务器愿意说话,卡住的位置在客户端协议层。

三、根因:端口与安全策略的配对
#

eM Client 的账号配置在 %APPDATA%\eM Client\accounts.dat,是一个 SQLite 库,表 AccountsJson 每行存一份配置的 JSON。把库里所有收发配置打出来对照,规律立刻出现:

import sqlite3, json, os
db = os.path.join(os.environ["APPDATA"], "eM Client", "accounts.dat")
con = sqlite3.connect("file:" + db.replace("\\", "/") + "?mode=ro", uri=True)
for _, v in con.execute("select key, value from AccountsJson"):
    d = json.loads(v)
    t, tv = d.get("Type", ""), d.get("TypeValue", {})
    if "AccountConfiguration" in t and ("Imap" in t or "Smtp" in t):
        print(tv.get("AccountName"), tv.get("Host"), tv.get("Port"), tv.get("SSL"))

用只读方式打开,不影响正在运行的客户端。结果节选(策略字段是 SSL):

账号 服务器 SSL 策略 结果
Gmail(多个账号) imap.gmail.com:993 UseTlsOnSpecialPort 正常
QQ 邮箱 imap.qq.com:993 UseTlsAlways 失败
另一家国内邮箱 imap.*.com:993 UseTlsAlways 失败
QQ 邮箱的 SMTP smtp.qq.com:587 UseTlsAlways 正常

所有能用的 993 账号用的都是 UseTlsOnSpecialPort,出问题的两条都用 UseTlsAlways。这两条还有另一个共同点:VersionString 是 10.1.4588.0,而库里其余配置全是 10.4.5674,也就是说它们是更早版本的设置向导建立的。

eM Client 的安全策略有三档,和端口要成对使用:

端口 正确的策略
993(IMAP)、465(SMTP) 在特殊端口上使用 SSL/TLS(旧式),UseTlsOnSpecialPort
143(IMAP) 如果可用,使用 SSL/TLS,UseTlsIfAvailable
25、587(SMTP) 强制使用 SSL/TLS,UseTlsAlways

UseTlsAlways(强制使用 SSL/TLS)走的是 STARTTLS 流程:连上以后先等服务器的明文问候,再协商加密。它在 143 和 587 上是对的,配到 993 上就会一直等一句不会出现的明文问候,正好对应日志里那句「30秒超时时间内没有服务器响应」。这也解释了为什么同一个账号发信始终正常:SMTP 走 587 加「强制使用」,本来就是正确配对。

四、修法与验证
#

界面上改三步:菜单「账户」选中该账号,进「IMAP」选项卡,把端口 993 右边的安全策略改成「在特殊端口上使用 SSL/TLS(旧式)」,保存并关闭。改完把账号重新上线即可,不需要重输密码(SMTP 能通过认证,说明授权码本身是对的)。

要批量改,或者界面进不去,也可以关掉客户端后直接改库。先备份 accounts.dat(连同 -wal、-shm 一起),再做一次精确替换:

con = sqlite3.connect(db)   # 路径同上,操作前先关闭 eM Client
for rid, val in list(con.execute("select rowid, value from AccountsJson")):
    if "ImapAccountConfiguration" in val and '"Port":993,"SSL":"UseTlsAlways"' in val:
        con.execute("update AccountsJson set value=? where rowid=?",
                    (val.replace('"Port":993,"SSL":"UseTlsAlways"',
                                 '"Port":993,"SSL":"UseTlsOnSpecialPort"'), rid))
con.commit()

替换前后的字符串都是合法 JSON,其它字段一个字节不动。改完重启客户端,用连接状态验收:

验收项 修复前 修复后
imap.qq.com:993 连接 每 30 秒超时断开一次,连续三次后账号离线 只建立一条连接,ESTABLISHED 常驻三分钟以上,没有重连循环
本地邮件库写入 无 mail_data.dat、folders.dat-wal 等持续被写入
另一处同错配的账号 每 10 分钟重试一次 连接常驻

只看客户端界面上「同步完成」不算验收,连接常驻加上本地邮件库有写入,才说明真的收到了信。

五、可复用清单
#

  1. 看 IMAP 明细日志(%APPDATA%\eM Client\Logs\):是不是卡满 30 秒、有没有 TLS 或认证报错。没有 TLS 报错,说明问题不在证书和密码上。
  2. 看代理或防火墙日志:命中直连规则且没有拨号失败,说明 TCP 层是通的。
  3. 用 openssl 或语言自带的 TLS 库做隐式 TLS 探测,确认服务器是否说话。不要用裸 TCP 探测 TLS 端口。
  4. 查 accounts.dat 里该账号的 Port 与 SSL 是否成对,993 和 465 必须是 UseTlsOnSpecialPort。
  5. 改完用「单条连接常驻」加「本地邮件库有写入」验收。

另外两个与本次故障无关,但同样容易出问题的点:

  • 账号设置里的「修复/诊断」按钮会把主机名改坏。日志里出现过 imap.imap.qq.com:993、mail.imap.qq.com:993 这类主机名,解析全部失败,是向导或诊断按钮拼出来的。主机名要手工确认为 imap.qq.com。
  • 单个账号的明细日志若开了三天以上,客户端启动时会弹窗询问是否关闭。抓日志时记得选保留,否则后面复现问题会没有证据。

(文中账号名、服务商与主机名做了替换或省略,方法与判断依据未改。)