症状: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 分钟重试一次 | 连接常驻 |
只看客户端界面上「同步完成」不算验收,连接常驻加上本地邮件库有写入,才说明真的收到了信。
五、可复用清单 #
- 看 IMAP 明细日志(
%APPDATA%\eM Client\Logs\):是不是卡满 30 秒、有没有 TLS 或认证报错。没有 TLS 报错,说明问题不在证书和密码上。 - 看代理或防火墙日志:命中直连规则且没有拨号失败,说明 TCP 层是通的。
- 用 openssl 或语言自带的 TLS 库做隐式 TLS 探测,确认服务器是否说话。不要用裸 TCP 探测 TLS 端口。
- 查
accounts.dat里该账号的Port与SSL是否成对,993 和 465 必须是UseTlsOnSpecialPort。 - 改完用「单条连接常驻」加「本地邮件库有写入」验收。
另外两个与本次故障无关,但同样容易出问题的点:
- 账号设置里的「修复/诊断」按钮会把主机名改坏。日志里出现过
imap.imap.qq.com:993、mail.imap.qq.com:993这类主机名,解析全部失败,是向导或诊断按钮拼出来的。主机名要手工确认为imap.qq.com。 - 单个账号的明细日志若开了三天以上,客户端启动时会弹窗询问是否关闭。抓日志时记得选保留,否则后面复现问题会没有证据。
(文中账号名、服务商与主机名做了替换或省略,方法与判断依据未改。)