MailKit 读取 IMAP 邮件时服务器意外断开连接
结论
从现有日志看,连接方式、认证和邮箱名称都正常。连接是在 Dovecot 返回第一封邮件内容时断开的,客户端没有收到完整的 IMAP 响应。
服务器先发送了:
* 1 FETCH (BODY[] {950}
这里的 {950} 表示服务器接下来应发送 950 个字节,然后结束 FETCH 响应,并返回类似下面的命令完成状态:
)
A00000006 OK Fetch completed
但实际日志在邮件内容传输到一半时就结束了。日志中没有结束括号、A00000006 OK,也没有正常的 BYE,所以 MailKit 报出:
The IMAP server has unexpectedly disconnected.
客户端日志无法说明服务器关闭连接的具体原因,还需要检查同一时间点的 Dovecot 或 Synology 邮件服务日志。可能的原因包括 Dovecot 进程异常退出、Maildir 文件损坏或被截断、索引中的邮件大小记录错误、存储读取失败或文件权限异常。
当前代码通过 GetMessage() 下载每封邮件的完整内容。如果只需要主题和日期,可以改用 Fetch() 获取邮件摘要。
为什么不是 993 或 143 端口问题
日志显示以下操作均已成功完成:
- TLS 连接建立成功;
- 用户认证成功;
- 可以列出
INBOX.subfolder; - 邮箱已以只读方式打开;
- 服务器返回了
93 EXISTS。
因此,993 端口配合 SecureSocketOptions.SslOnConnect 可以正常工作。改用 143 端口无法解决读取第一封邮件时服务器断开连接的问题。
如果要使用 143 端口,并且服务器支持 STARTTLS,通常应这样连接:
client.Connect(mailserver, 143, SecureSocketOptions.StartTls);
SecureSocketOptions.None 使用明文连接,只应在明确受控的本地测试环境中使用。
只获取主题和日期
可以一次请求所有邮件的 ENVELOPE 和内部接收日期,无需逐封下载正文:
using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
public static void CheckImap()
{
using var client = new ImapClient(
new ProtocolLogger(@"d:\temp\imap.log"));
try
{
client.Connect(
mailserver,
993,
SecureSocketOptions.SslOnConnect);
client.Authenticate(username, password);
var folder = client.GetFolder("INBOX.subfolder");
folder.Open(FolderAccess.ReadOnly);
Console.WriteLine("Total messages: {0}", folder.Count);
var uids = folder.Search(SearchQuery.All);
var summaries = folder.Fetch(
uids,
MessageSummaryItems.UniqueId |
MessageSummaryItems.Envelope |
MessageSummaryItems.InternalDate);
foreach (var summary in summaries)
{
var subject = summary.Envelope?.Subject ?? "(no subject)";
// Date 是邮件头中的 Date;缺失时使用服务器内部日期。
DateTimeOffset? date =
summary.Envelope?.Date ?? summary.InternalDate;
Console.WriteLine(
"UID {0}: {1:O}\t{2}",
summary.UniqueId,
date,
subject);
}
}
catch (Exception ex)
{
// 不要只打印 ex.Message,完整异常可能包含内层异常和调用栈。
Console.Error.WriteLine(ex);
Console.Error.WriteLine(
"Connected: {0}, Authenticated: {1}",
client.IsConnected,
client.IsAuthenticated);
}
finally
{
if (client.IsConnected)
client.Disconnect(true);
}
}
Envelope.Subject 是邮件主题,Envelope.Date 取自邮件的 Date 头。InternalDate 则是 IMAP 服务器记录的内部日期,两者含义不同。上面的代码仅在邮件头日期缺失时使用 InternalDate。
长期保存邮件标识时,UID 也比邮件序号可靠。删除邮件或重新打开邮箱后,邮件序号可能发生变化;在同一个 UIDVALIDITY 周期内,UID 会保持稳定。
如何定位具体故障
1. 查看服务器日志
运行测试代码时记下准确时间,再检查对应时段的 Dovecot 或 Synology 邮件服务日志。可以重点搜索:
panic
fatal
segfault
I/O error
permission denied
corrupted
short read
disconnected
客户端日志只能确认服务器或中间连接提前中断,具体原因通常要从服务端日志中查找。
2. 判断是否只影响第一封邮件
当前的 GetMessage(0) 对应协议中的 FETCH 1。可以先单独请求第一封邮件的摘要:
var summaries = folder.Fetch(
0,
0,
MessageSummaryItems.UniqueId |
MessageSummaryItems.Envelope |
MessageSummaryItems.InternalDate);
如果摘要可以正常返回,但 GetMessage(0) 仍会导致断开,问题很可能出在读取这封邮件的完整文件时。
如果请求摘要也会断开,则需要检查 Dovecot 的索引、缓存以及这封邮件的元数据。
也可以逐封测试后面的邮件:
for (int i = 0; i < folder.Count; i++)
{
try
{
var summaries = folder.Fetch(
i,
i,
MessageSummaryItems.UniqueId |
MessageSummaryItems.Envelope);
var summary = summaries.FirstOrDefault();
Console.WriteLine(
"Index {0}, UID {1}, Subject: {2}",
i,
summary?.UniqueId,
summary?.Envelope?.Subject);
}
catch (Exception ex)
{
Console.Error.WriteLine("Index {0}: {1}", i, ex);
break;
}
}
连接断开后,不能继续使用同一个 ImapClient 测试下一封邮件。每次都需要重新连接、认证并打开文件夹。
3. 检查 Maildir 和 Dovecot 索引
如果拥有 NAS 管理权限,可以检查第一封邮件对应的 Maildir 文件:
- 文件能否完整读取;
- 文件大小是否与 Dovecot 的记录一致;
- Dovecot 运行账户是否有读取权限;
- 文件所在磁盘是否出现 I/O 错误;
- 文件是否被同步程序、杀毒软件或备份程序截断。
确认邮件文件完好并完成备份后,可以考虑让 Dovecot 重建该邮箱的索引。例如,标准 Dovecot 环境通常提供以下命令:
doveadm force-resync -u 'user@example.com' 'INBOX.subfolder'
Synology 套件可能调整命令位置、用户名格式或邮箱配置,因此要先确认设备实际采用的 Dovecot 管理方式。不要直接删除索引或 Maildir 文件。
4. 不要把 Thunderbird 的显示结果当作服务端完整读取测试
Thunderbird 可能已经缓存了邮件正文。即使界面能够显示邮件,也不能证明它刚刚从服务器成功执行了相同的 BODY.PEEK[] 请求。
可以清除本地缓存后重新同步,也可以查看 Thunderbird 的 IMAP 协议日志,确认它是否重新从服务器下载了第一封邮件。
Fetch() 能否绕过问题
如果服务器只在读取完整正文时出错,使用摘要版 Fetch() 或许可以满足“列出主题和日期”的需求,同时减少网络传输量。
但服务器本身的故障仍然存在。只要 Dovecot 在处理合法的 BODY.PEEK[] 请求时仍会提前关闭连接,完整邮件读取就没有恢复。需要结合服务端日志,修复有问题的邮件文件、索引、权限或存储。
协议日志可能包含邮件内容、地址和其他敏感头字段。排查结束后,应删除日志文件或限制其访问权限。
备注:内容仅供参考。
