介绍
MimeKit和MailKit是.NET中流行的功能齐全的电子邮件框架
Feature
- 安全
- SASL身份验证(支持CRAM-MD5、DIGEST-MD5、LOGIN、NTLM、OAUTHBEARER、PLAIN、SCRAM-SHA-1、SCRAM-SHA-256、SCRAM-SHA-512和XOAUTH2机制。)
- 支持S/MIME v3.2。
- 支持OpenPGP。
- 支持DKIM签名。
- 支持ARC签名。
- SMTP Client
- 支持SSL和TLS。
- 支持STARTTLS、SIZE、DSN、8BITMIME、pipeline、BINARYMIME和SMTPUTF8扩展。
- POP3 Client
- 支持SSL和TLS。
- 支持STLS、UIDL、流水线、UTF8和LANG扩展。
- IMAP Client
- 支持SSL和TLS。
- 支持ACL、QUOTA、LITERAL+、IDLE、NAMESPACE、ID、CHILDREN、LOGINDISABLED、STARTTLS、MULTIAPPEND、UNSELECT、UIDPLUS、CONDSTORE、ESEARCH、SASL-ID、COMPRESS、WITHIN、ENABLE、QRESYNC、SORT、THREAD、ANNOTATE、LIST-EXTENDED、ESORT、METADATA、METADATA-SERVER、NOTIFY、FILTERS、LIST-STATUS、SORT=DISPLAY、SPECIAL-USE、CREATE-SPECIAL-USE、SEARCH=FUZZY,MOVE、UTF8=ACCEPT、UTF8=ONLY、LITERAL-、APPENDLIMIT、STATUS=SIZE、OBJECTID、REPLACE、SAVEDATE、XLIST和Google Mail(X-GM-EXT-1)扩展名。
- MIME解析器所有可能阻止的API都允许通过CancellationToken取消。
- 灵活:允许覆盖任何MIME类型的默认类。
- 高性能:比市场上任何其他.NET MIME解析器都快。竞争对手甚至表现最快的C++解析器。
- 健壮:轻松处理各种各样的损坏的MIME格式。
- 所有可能中断的API都允许通过CancellationToken取消。
- 所有执行I/O的API都有异步变体。
- 客户端消息的排序和线程处理。
- 支持.NET 4.5、.NET 4.6、.NET 4.7、.NET 4.8、.NET 5.0、.NETStandard 2.0、Xamarin.Android、Xamarin.iOS、Windows Phone 8.1等。
History
作为电子邮件客户机的开发人员和用户,我逐渐意识到绝大多数电子邮件客户机(和服务器)软件的MIME实现都不尽如人意。通常情况下,这些电子邮件客户端会创建损坏的MIME消息和/或错误地尝试解析MIME消息,从而从MIME本应提供的全部好处中减去。MimeKit旨在通过尽可能地遵循MIME规范来解决这个问题,同时也为程序员提供了一个非常易于使用的高级API。
这使我一开始实现了另一个名为GMime的MIME解析器库,它是用C实现的,后来又添加了一个名为GMime Sharp的C#绑定。
现在我发现自己通常使用C语言而不是像C这样的低级语言,我决定开始用C语言编写一个新的解析器,它不依赖于GMime。这也允许我有更多的灵活性,因为我可以使用泛型并创建一个更符合.NET的API。
Performance
虽然主流信念可能表明,C ^永远不能像C一样快,但事实证明,随着一点创造性的解析器设计和一些巧妙的优化[1 ] [2 ],MimeKit的性能实际上与GMime相当。
由于GMime是众所周知的高性能本机MIME解析器,并且MimeKit或多或少与GMime的性能匹配,因此MimeKit在.NET MIME解析器领域的性能可能是无与伦比的。
作为比较,正如我在这里写的(我已经优化了至少30%的MimeKit),MimeKit比OpenPOP.NET快25倍,比SharpMimeTools快75倍,比基于regex的解析器快65倍。即使是商业的MIME解析器产品,比如LimiLabs的Mail.dll和NewtonIdeas的Mime4Net,也无法接近MimeKit的性能(它们都比MimeKit慢几个数量级)。
入门
Creating messages
MimeKit提供了许多创建消息的方法
1、Creating a Message with Attachments
var message = new MimeMessage (); message.From.Add (new MailboxAddress ("Joey", "joey@friends.com")); message.To.Add (new MailboxAddress ("Alice", "alice@wonderland.com")); message.Subject = "How you doin?"; // create our message text, just like before (except don't set it as the message.Body) var body = new TextPart ("plain") { Text = @"Hey Alice, What are you up to this weekend? Monica is throwing one of her parties on Saturday and I was hoping you could make it. Will you be my +1? -- Joey " }; // create an image attachment for the file located at path var attachment = new MimePart ("image", "gif") { Content = new MimeContent (File.OpenRead (path), ContentEncoding.Default), ContentDisposition = new ContentDisposition (ContentDisposition.Attachment), ContentTransferEncoding = ContentEncoding.Base64, FileName = Path.GetFileName (path) }; // now create the multipart/mixed container to hold the message text and the // image attachment var multipart = new Multipart ("mixed"); multipart.Add (body); multipart.Add (attachment); // now set the multipart/mixed as the message body message.Body = multipart;
其中:
TextPart:是具有文本媒体类型的MIME部件的叶子节点。TextPart构造函数的第一个参数指定媒体子类型(或者用枚举的构造函数),在本例中为plain。您可能熟悉的另一种媒体子类型是html子类型。【通常,对于纯文本内容,子类型应该是“plain”,对于html内容,子类型应该是“html”。】 TextPart继承自MimePart。
其他一些例子包括enriched、rtf和xml。Text属性是获取和设置MIME部分的字符串内容的最简单方法。
附件:
附件与任何其他MimePart一样,唯一的区别是它们通常有一个值为attachment的Content-Disposition头,而不是inline或根本没有Content-Disposition头。
MimePart:包含内容(如消息正文文本或附件)的叶节点MIME部件。
当然,这只是一个简单的例子。许多现代邮件客户端(如Outlook或Thunderbird)都会发送text/html和text/plain版本的消息文本。为此,您需要为每个部分创建一个TextPart,然后将它们添加到一个multipart/alternative中,如下所示:
var attachment = CreateImageAttachment (); var plain = CreateTextPlainPart (); var html = CreateTextHtmlPart (); // Note: it is important that the text/html part is added second, because it is the // most expressive version and (probably) the most faithful to the sender's WYSIWYG // editor. var alternative = new MultipartAlternative (); alternative.Add (plain); alternative.Add (html); // now create the multipart/mixed container to hold the multipart/alternative // and the image attachment var multipart = new Multipart ("mixed"); multipart.Add (alternative); multipart.Add (attachment); // now set the multipart/mixed as the message body message.Body = multipart;
Multipart:一种多部分MIME实体,可以包含一组MIME实体。
所有多部分MIME实体都将具有媒体类型为“multipart”的Content-Type。电子邮件中使用的最常见的多部分MIME实体是“multipart/mixed”实体。
在最初的MIME规范中定义了四(4)个初始子类型:mixed、alternative、digest和parallel。
- “multipart/mixed”类型是一种通用容器。在电子邮件中使用时,第一个实体通常是邮件的“正文”,而其他实体通常是文件附件。
- 说到消息“bodies”,“multipart/alternative”类型用于提供消息主体的可选格式列表(通常是“text/plain”和“text/html”)。这些替代方法是为了增加对原始文档的忠实度(换句话说,最后一个实体的格式将在呈现时与发送客户端的WYSISYG编辑器生成的内容最接近)。
- “multipart/digest”类型通常包含MIME消息的摘要,最常用于邮件列表软件。
- “multipart/parallel”类型包含所有要并行显示(或听到)的实体。
另一种常用的类型是“multipart/related”类型,如人们所料,它包含相互关联的MIME部分,这些部分通常基于内容Id和/或内容位置头通过uri相互引用。
2、Using a BodyBuilder
如果您习惯使用System.Net.Mail的API来创建消息,您可能会发现使用BodyBuilder比手动创建MIME部件树更友好。下面是如何使用BodyBuilder创建消息正文:
var message = new MimeMessage (); message.From.Add (new MailboxAddress ("Joey", "joey@friends.com")); message.To.Add (new MailboxAddress ("Alice", "alice@wonderland.com")); message.Subject = "How you doin?"; var builder = new BodyBuilder (); // Set the plain-text version of the message text builder.TextBody = @"Hey Alice, What are you up to this weekend? Monica is throwing one of her parties on Saturday and I was hoping you could make it. Will you be my +1? -- Joey "; // In order to reference selfie.jpg from the html text, we'll need to add it // to builder.LinkedResources and then use its Content-Id value in the img src. var image = builder.LinkedResources.Add (@"C:\Users\Joey\Documents\Selfies\selfie.jpg"); image.ContentId = MimeUtils.GenerateMessageId (); // Set the html version of the message text builder.HtmlBody = string.Format (@"<p>Hey Alice,<br> <p>What are you up to this weekend? Monica is throwing one of her parties on Saturday and I was hoping you could make it.<br> <p>Will you be my +1?<br> <p>-- Joey<br> <center><img src=""cid:{0}""></center>", image.ContentId); // We may also want to attach a calendar event for Monica's party... builder.Attachments.Add (@"C:\Users\Joey\Documents\party.ics"); // Now we just need to set the message body and we're done message.Body = builder.ToMessageBody ();
为了从html文本中引用图片,我们需要将它添加到builder.LinkedResources中,然后在img src中使用它的Content Id值
Parsing messages
MimeKit的一个更常见的操作是解析来自任意流的电子邮件。完成这项任务有两种方法。
1、Using the Load methods
最简单的方法是在mimessage上使用一种Load方法。
// Load a MimeMessage from a stream var message = MimeMessage.Load (stream);
2、Using MimeParser directly
第二种方法是使用MimeParser类。在大多数情况下,除非您希望解析unix mbox文件流,否则不需要直接使用MimeParser。您可以这样做:
// Load a MimeMessage from a stream var parser = new MimeParser (stream, MimeFormat.Entity); var message = parser.ParseMessage ();
对于Unix mbox文件流,您将使用如下解析器:
// Load every message from a Unix mbox var parser = new MimeParser (stream, MimeFormat.Mbox); while (!parser.IsEndOfStream) { var message = parser.ParseMessage (); // do something with the message }
Working with messages
MimeKit提供了许多从消息中获取所需数据的方法。
1、消息结构
对电子邮件的一个常见误解是:有一个定义良好的邮件正文,然后是一个附件列表。事实并非如此。事实上,MIME是一种树结构的内容,很像一个文件系统。
幸运的是,MIME确实定义了一组通用规则,用于说明邮件客户端应该如何解释MIME部分的树结构。Content-Disposition头旨在向接收客户端提供提示,说明哪些部分将显示为消息体的一部分,哪些部分将被解释为附件。
content-disposition头通常有两个值之一:inline 或 attachment。
这些值的意义应该相当明显:
- 如果该值是attachment,那么所述MIME部分的内容将被表示为与核心消息分离的文件附件。
- 如果该值是inline,那么该MIME部分的内容将在邮件客户端对核心消息体的呈现中内联显示。
- 如果Content-Disposition头不存在,则应将其视为内联值。
从技术上讲,缺少Content-Disposition头或标记为inline的每个部分都是核心消息体的一部分。
不过,事情远不止这些。
现代MIME消息通常包含一个multipart/alternative MIME容器,该容器通常包含发送者编写的文本的text/plain和text/html版本。text/html版本的格式通常比text/plain版本更接近发送者在所见即所得编辑器中看到的内容。
以这两种格式发送消息文本的原因是,并非所有的邮件客户端都能够显示HTML。
接收客户端应仅显示 multipart/alternative容器中包含的可选视图之一。由于备选视图是按照发送者在所见即所得编辑器中看到的内容从最不忠实到最忠实的顺序列出的,因此接收客户端应该从末尾开始遍历备选视图列表,并向后操作,直到找到能够显示的部分。
Example:
multipart/alternative text/plain text/html
如上面的示例所示,text/html部分列在最后,因为它最忠实于发送者在编写消息时在所见即所得编辑器中看到的内容。
更为复杂的是,有时现代邮件客户端会使用multipart/related的MIME容器,而不是简单的text/html部分,以便在html中嵌入图像和其他多媒体内容。
Example:
multipart/alternative
text/plain
multipart/related
text/html
image/jpeg
video/mp4
image/png
在上面的示例中,备选视图之一是multipart/related容器,其中包含引用兄弟视频和图像的消息体的HTML版本。
现在您已经大致了解了消息的结构以及如何解释各种MIME实体,下一步是学习如何使用MimeKit遍历MIME树。
正文Body是消息的顶级MIME entity。一般来说,它要么是TextPart,要么是Multipart。
使用MimeKit迭代消息的树结构有3种方法。
第一种方法是递归遍历MIME结构,如下所示:
static void HandleMimeEntity (MimeEntity entity) { var multipart = entity as Multipart; if (multipart != null) { for (int i = 0; i < multipart.Count; i++) HandleMimeEntity (multipart[i]); return; } var rfc822 = entity as MessagePart; if (rfc822 != null) { var message = rfc822.Message; HandleMimeEntity (message.Body); return; } var part = (MimePart) entity; // do something with the MimePart, such as save content to disk }