本文使用稿仓编辑发布(DraftDepot:https://github.com/xland/DraftDepot,稿仓可以一键把文章同步到多个平台)
如果你是 C#.NET 开发者
使用 System.Net.Mail 模块是不现实的,因为它仅支持 SMTP 协议且功能有限,不支持 IMAP/POP3,不适合做完整的邮箱客户端。
使用开源的 MailKit(https://github.com/jstedfast/MailKit) 是最佳选择。
邮件协议(SMTP/IMAP/POP3)的标准文档(RFC)虽然写得很清楚,但现实世界中的邮件服务器实现千奇百怪。

IMAP 协议实现的混乱
IMAP 是最复杂的邮件协议,也是怪癖的重灾区:
非标准响应格式: 很多服务器的 FETCH、SEARCH、LIST 响应不符合 RFC 3501/9051 规范。
MailKit 内置了宽松的解析器,能容忍多余空格、缺失括号、非法字符等。
UID 与序列号混淆: 有些服务器在应该返回 UID 的地方返回了序列号,或者反之。MailKit 会自动检测并纠正。
CAPABILITY 撒谎: 某些服务器声称支持某个扩展(如 CONDSTORE、QRESYNC),但实际使用时会报错或返回错误数据。MailKit 维护了一个已知问题服务器列表,会自动禁用有问题的扩展。
NAMESPACE 异常: 部分服务器(如某些旧版 Exchange、Domino)返回的命名空间格式不标准,MailKit 做了大量特殊适配。
SEARCH 语法差异: 不同服务器对 SEARCH 命令的日期格式、字符串编码、逻辑运算符的支持程度不同,MailKit 会根据服务器能力自动降级查询策略。
SMTP 发送的兼容性问题
EHLO/HELO 降级: 服务器声称支持 EHLO 但实际响应错误时,自动回退到 HELO。
PIPELINING 陷阱: 有些服务器 advertise PIPELINING 但实际上不支持或处理有误,MailKit 能检测并安全降级。
8BITMIME / BINARYMIME: 服务器声明支持但实际传输中截断或损坏二进制数据,MailKit 会自动转为 Base64/Quoted-Printable 编码。
SIZE 限制误报: 服务器报告的 SIZE 上限不准确,MailKit 有更智能的分块和重试机制。
非标准状态码: 很多服务器返回的 SMTP 状态码不符合 RFC 3463 增强状态码规范,MailKit 能正确解读各种变体。
认证与安全层怪癖
SASL 机制实现错误: 服务器的 AUTH 响应格式不对、challenge 编码异常等。
STARTTLS 问题: 某些服务器在 STARTTLS 后仍发送明文响应,或在 TLS 握手时有非标准行为。
OAuth2 变体: Gmail、Microsoft 365、Yahoo 等的 OAuth2 流程各有微妙差异,MailKit 都做了专门适配。
基础设施层面的健壮性
连接超时与重试: 针对不同服务器的响应速度差异,有自适应的超时策略。
IDLE 命令保活: IMAP IDLE 在不同服务器上的行为差异很大(有的不发 * OK,有的提前断开),MailKit 有完善的 keep-alive 和重连机制。
字面量 (Literal) 大小写: IMAP 字面量 {123} vs {123+} 的处理差异。
MailKit 的作者 Jeffrey Stedfast 花了十几年时间收集并处理了这些“服务端怪癖”,使其成为目前兼容性最强的 .NET 邮件库。
一个邮箱客户端,除了收发邮件外,还得能很好的解析和构造邮件。
MimeKit (https://github.com/jstedfast/MimeKit)是 .NET 平台上最强大、最严谨的 MIME (Multipurpose Internet Mail Extensions) 解析与构建库。
它是 MailKit 的底层依赖,但本身也是一个完全独立、可单独使用的库。
如果说 MailKit 负责“运输”(SMTP/IMAP/POP3),那么 MimeKit 就负责邮件的“打包和解包”。
它的设计哲学是:严格遵循 RFC 标准进行构建,极度宽容地解析。“发送时保守,接收时开放”。
MIME / 消息解析的容错
非法字符集声明: Content-Type 声明了 charset=gb2312 但实际内容是 UTF-8(或反之),MimeKit 会自动探测真实编码。
破损的 Base64 / QP 编码: 换行位置错误、非法字符、padding 缺失等,都能尽力恢复而非直接抛异常。
非标准头部折叠: 头部续行的空白符不规范、缺少空格等。
重复/冲突头部: 多个 Content-Type、多个 From 等,按优先级和上下文智能选择。
TNEF (winmail.dat): 微软 Exchange 特有的封装格式,MimeKit 原生支持解码。
缺点
站在产品的角度,MimeKit几乎是完美的,所有问题都是.NET框架带来的
.NET内存占用高、没有完美的界面GUI框架(几乎只能用WebView替代)
AOT不友好(MailKit/MimeKit 大量使用反射)
邮件解析是典型的“短生命周期大对象”场景。大量临时 buffer 会导致 Full GC 频率增加
MimeKit 收发邮件时要频繁调用操作系统原生库,每次跨越托管/非托管边界(P/Invoke)都有固定开销,且在高频小数据场景下(如逐块验证签名)累积显著。
下面是使用MailKit发送邮件的代码:
using System;
using MailKit.Net.Smtp;
using MailKit;
using MimeKit;
namespace TestClient {
class Program
{
public static void Main (string[] args)
{
var message = new MimeMessage ();
message.From.Add (new MailboxAddress ("Joey Tribbiani", "joey@friends.com"));
message.To.Add (new MailboxAddress ("Mrs. Chanandler Bong", "chandler@friends.com"));
message.Subject = "How you doin'?";
message.Body = new TextPart ("plain") {
Text = @"Hey Chandler,
I just wanted to let you know that Monica and I were going to go play some paintball, you in?
-- Joey"
};
using (var client = new SmtpClient ()) {
client.Connect ("smtp.friends.com", 587, false);
// Note: only needed if the SMTP server requires authentication
client.Authenticate ("joey", "password");
client.Send (message);
client.Disconnect (true);
}
}
}
}
下面是使用POP3接收邮件的代码
using System;
using MailKit.Net.Pop3;
using MailKit;
using MimeKit;
namespace TestClient {
class Program
{
public static void Main (string[] args)
{
using (var client = new Pop3Client ()) {
client.Connect ("pop.friends.com", 110, false);
client.Authenticate ("joey", "password");
for (int i = 0; i < client.Count; i++) {
var message = client.GetMessage (i);
Console.WriteLine ("Subject: {0}", message.Subject);
}
client.Disconnect (true);
}
}
}
}
下面是使用IMAP接收邮件的代码
using System;
using MimeKit;
using MailKit;
using MailKit.Search;
using MailKit.Net.Imap;
namespace TestClient {
class Program
{
public static void Main (string[] args)
{
using (var client = new ImapClient ()) {
client.Connect ("imap.friends.com", 993, true);
client.Authenticate ("joey", "password");
// The Inbox folder is always available on all IMAP servers...
var inbox = client.Inbox;
inbox.Open (FolderAccess.ReadOnly);
Console.WriteLine ("Total messages: {0}", inbox.Count);
Console.WriteLine ("Recent messages: {0}", inbox.Recent);
for (int i = 0; i < inbox.Count; i++) {
var message = inbox.GetMessage (i);
Console.WriteLine ("Subject: {0}", message.Subject);
}
client.Disconnect (true);
}
}
}
}