关于电子邮件的一个常见误解是有一个明确定义的邮件正文,然后是一个附件列表。事实并非如此。事实上,MIME 是一种内容树结构,很像文件系统。
幸运的是,MIME 确实为邮件客户端应如何解释这种 MIME 部分的树结构定义了一组通用规则。标Content-Disposition
头旨在向接收客户端提供有关哪些部分应显示为消息正文的一部分以及哪些应被解释为附件的提示。
标Content-Disposition
头通常具有以下两个值之一:inline
或attachment
。
这些值的含义应该是相当明显的。如果值为attachment
,则所述 MIME 部分的内容将作为与核心消息分开的文件附件呈现。但是,如果值为inline
,则该 MIME 部分的内容将在邮件客户端呈现的核心消息正文中内联显示。如果Content-Disposition
标头不存在,则应将其视为值是inline
。
从技术上讲,缺少Content-Disposition
标头或标记为的每个部分inline
都是核心消息体的一部分。
不过,还有更多的东西。
现代 MIME 消息通常包含一个multipart/alternative
MIME 容器,该容器通常包含发件人编写的文本的一个text/plain
版本text/html
。与text/html
版本相比,版本的格式通常更接近发件人在其所见即所得编辑器中看到的内容text/plain
。
以两种格式发送消息文本的原因是并非所有邮件客户端都能够显示 HTML。
接收客户端应该只显示容器中multipart/alternative
包含的替代视图之一。由于替代视图是按照发送者在其所见即所得编辑器中看到的内容按照最不忠实到最忠实的顺序列出的,因此接收客户端应该从末尾开始遍历替代视图列表并向后工作,直到找到它的部分能够显示。
例子:
multipart/alternative
text/plain
text/html
如上例所示,该text/html
部分列在最后,因为它最忠实于发件人在编写消息时在其所见即所得编辑器中看到的内容。
更复杂的是,有时现代邮件客户端会使用multipart/related
MIME 容器而不是简单的text/html
部分,以便在 HTML 中嵌入图像和其他多媒体内容。
例子:
multipart/alternative
text/plain
multipart/related
text/html
image/jpeg
video/mp4
image/png
在上面的示例中,替代视图之一是一个multipart/related
容器,其中包含引用同级视频和图像的消息正文的 HTML 版本。
现在您已经大致了解了消息的结构以及如何解释各种 MIME 实体,我们可以开始弄清楚如何按预期实际呈现消息。
使用 MimeVisitor(呈现消息的最准确方式)
MimeKit 包括一个MimeVisitor
用于访问 MIME 树结构中每个节点的类。例如,以下MimeVisitor
子类可用于生成要由浏览器控件(例如WebBrowser
)呈现的 HTML:
/// <summary>
/// Visits a MimeMessage and generates HTML suitable to be rendered by a browser control.
/// </summary>
class HtmlPreviewVisitor : MimeVisitor
{
List<MultipartRelated> stack = new List<MultipartRelated> ();
List<MimeEntity> attachments = new List<MimeEntity> ();
readonly string tempDir;
string body;
/// <summary>
/// Creates a new HtmlPreviewVisitor.
/// </summary>
/// <param name="tempDirectory">A temporary directory used for storing image files.</param>
public HtmlPreviewVisitor (string tempDirectory)
{
tempDir = tempDirectory;
}
/// <summary>
/// The list of attachments that were in the MimeMessage.
/// </summary>
public IList<MimeEntity> Attachments {
get { return attachments; }
}
/// <summary>
/// The HTML string that can be set on the BrowserControl.
/// </summary>
public string HtmlBody {
get { return body ?? string.Empty; }
}
protected override void VisitMultipartAlternative (MultipartAlternative alternative)
{
// walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful
for (int i = alternative.Count - 1; i >= 0 && body == null; i--)
alternative[i].Accept (this);
}
protected override void VisitMultipartRelated (MultipartRelated related)
{
var root = related.Root;
// push this multipart/related onto our stack
stack.Add (related);
// visit the root document
root.Accept (this);
// pop this multipart/related off our stack
stack.RemoveAt (stack.Count - 1);
}
// look up the image based on the img src url within our multipart/related stack
bool TryGetImage (string url, out MimePart image)
{
UriKind kind;
int index;
Uri uri;
if (Uri.IsWellFormedUriString (url, UriKind.Absolute))
kind = UriKind.Absolute;
else if (Uri.IsWellFormedUriString (url, UriKind.Relative))
kind = UriKind.Relative;
else
kind = UriKind.RelativeOrAbsolute;
try {
uri = new Uri (url, kind);
} catch {
image = null;
return false;
}
for (int i = stack.Count - 1; i >= 0; i--) {
if ((index = stack[i].IndexOf (uri)) == -1)
continue;
image = stack[i][index] as MimePart;
return image != null;
}
image = null;
return false;
}
// Save the image to our temp directory and return a "file://" url suitable for
// the browser control to load.
// Note: if you'd rather embed the image data into the HTML, you can construct a
// "data:" url instead.
string SaveImage (MimePart image, string url)
{
string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_');
string path = Path.Combine (tempDir, fileName);
if (!File.Exists (path)) {
using (var output = File.Create (path))
image.ContentObject.DecodeTo (output);
}
return "file://" + path.Replace ('\\', '/');
}
// Replaces <img src=...> urls that refer to images embedded within the message with
// "file://" urls that the browser control will actually be able to load.
void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter)
{
if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) {
ctx.WriteTag (htmlWriter, false);
// replace the src attribute with a file:// URL
foreach (var attribute in ctx.Attributes) {
if (attribute.Id == HtmlAttributeId.Src) {
MimePart image;
string url;
if (!TryGetImage (attribute.Value, out image)) {
htmlWriter.WriteAttribute (attribute);
continue;
}
url = SaveImage (image, attribute.Value);
htmlWriter.WriteAttributeName (attribute.Name);
htmlWriter.WriteAttributeValue (url);
} else {
htmlWriter.WriteAttribute (attribute);
}
}
} else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) {
ctx.WriteTag (htmlWriter, false);
// add and/or replace oncontextmenu="return false;"
foreach (var attribute in ctx.Attributes) {
if (attribute.Name.ToLowerInvariant () == "oncontextmenu")
continue;
htmlWriter.WriteAttribute (attribute);
}
htmlWriter.WriteAttribute ("oncontextmenu", "return false;");
} else {
// pass the tag through to the output
ctx.WriteTag (htmlWriter, true);
}
}
protected override void VisitTextPart (TextPart entity)
{
TextConverter converter;
if (body != null) {
// since we've already found the body, treat this as an attachment
attachments.Add (entity);
return;
}
if (entity.IsHtml) {
converter = new HtmlToHtml {
HtmlTagCallback = HtmlTagCallback
};
} else if (entity.IsFlowed) {
var flowed = new FlowedToHtml ();
string delsp;
if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp))
flowed.DeleteSpace = delsp.ToLowerInvariant () == "yes";
converter = flowed;
} else {
converter = new TextToHtml ();
}
body = converter.Convert (entity.Text);
}
protected override void VisitTnefPart (TnefPart entity)
{
// extract any attachments in the MS-TNEF part
attachments.AddRange (entity.ExtractAttachments ());
}
protected override void VisitMessagePart (MessagePart entity)
{
// treat message/rfc822 parts as attachments
attachments.Add (entity);
}
protected override void VisitMimePart (MimePart entity)
{
// realistically, if we've gotten this far, then we can treat this as an attachment
// even if the IsAttachment property is false.
attachments.Add (entity);
}
}
您使用此访问者的方式可能如下所示:
void Render (MimeMessage message)
{
var tmpDir = Path.Combine (Path.GetTempPath (), message.MessageId);
var visitor = new HtmlPreviewVisitor (tmpDir);
Directory.CreateDirectory (tmpDir);
message.Accept (visitor);
DisplayHtml (visitor.HtmlBody);
DisplayAttachments (visitor.Attachments);
}
使用TextBody
andHtmlBody
属性(最简单的方法)
为了简化获取消息文本的常见任务,MimeMessage
包括两个可以帮助您获取消息正文的text/plain
或text/html
版本的属性。它们分别是TextBody
和HtmlBody
。
但是请记住,至少对于该HtmlBody
属性,HTML 部分可能是 a 的子级multipart/related
,允许它引用也包含在该multipart/related
实体中的图像和其他类型的媒体。这个属性实际上只是一个方便属性,并不能很好地替代您自己遍历 MIME 结构以便您可以正确解释相关内容。