PAYATHON 2026

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[] 请求时仍会提前关闭连接,完整邮件读取就没有恢复。需要结合服务端日志,修复有问题的邮件文件、索引、权限或存储。

协议日志可能包含邮件内容、地址和其他敏感头字段。排查结束后,应删除日志文件或限制其访问权限。

备注:内容仅供参考。