電子メールに関する一般的な誤解は、明確に定義されたメッセージ本文と添付ファイルのリストがあるというものです。これは実際には当てはまりません。実際には、MIME はコンテンツのツリー構造であり、ファイル システムによく似ています。
幸いなことに、MIME では、メール クライアントがこの MIME パーツのツリー構造をどのように解釈するかについて、一連の一般的な規則が定義されています。ヘッダーは、Content-Disposition
どの部分がメッセージ本文の一部として表示されることを意図しており、どの部分が添付ファイルとして解釈されることを意図しているかについて、受信クライアントにヒントを提供することを目的としています。
ヘッダーは通常、またはContent-Disposition
の 2 つの値のinline
いずれかになりattachment
ます。
これらの値の意味は明らかです。値が の場合、attachment
MIME 部分のコンテンツは、コア メッセージとは別の添付ファイルとして表示されることを意味します。ただし、値が の場合、inline
その MIME 部分のコンテンツは、メール クライアントのコア メッセージ本文のレンダリング内にインラインで表示されることを意味します。Content-Disposition
ヘッダーが存在しない場合は、値が であるかのように処理する必要がありますinline
。
Content-Disposition
技術的には、ヘッダーがない、または としてマークされているすべての部分inline
は、コア メッセージ本文の一部です。
ただし、それだけではありません。
最近の MIME メッセージには、通常、送信者が書いたテキストのバージョンを含むmultipart/alternative
MIME コンテナが含まれていることがよくあります。通常、バージョンは、送信者が WYSIWYG エディターで見たバージョンよりもはるかに近い形式になっています。text/plain
text/html
text/html
text/plain
メッセージ テキストを両方の形式で送信する理由は、すべてのメール クライアントが HTML を表示できるわけではないためです。
受信クライアントは、コンテナー内に含まれる代替ビューの 1 つだけを表示する必要がありますmultipart/alternative
。代替ビューは、送信者が WYSIWYG エディタで見たものに最も忠実でないものから最も忠実なものの順にリストされているため、受信側のクライアントは、代替ビューのリストを最後から始めて、その部分が見つかるまで逆方向に作業する必要があります。表示可能です。
例:
multipart/alternative
text/plain
text/html
上記の例に見られるようにtext/html
、送信者がメッセージを作成するときに WYSIWYG エディターで見たものに最も忠実であるため、パーツは最後にリストされています。
さらに複雑なことに、最新のメール クライアントは、HTML 内に画像やその他のマルチメディア コンテンツを埋め込むためにmultipart/related
、単純なパーツではなく MIME コンテナーを使用することがあります。text/html
例:
multipart/alternative
text/plain
multipart/related
text/html
image/jpeg
video/mp4
image/png
上記の例では、代替ビューの 1 つは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
およびHtmlBody
プロパティの使用(最も簡単な方法)
メッセージのテキストを取得する一般的なタスクを簡素化するために、メッセージ本文のまたはバージョンMimeMessage
を取得するのに役立つ 2 つのプロパティが含まれています。これらは、それぞれ と です。text/plain
text/html
TextBody
HtmlBody
ただし、少なくともHtmlBody
プロパティでは、HTML 部分が の子である可能性があることに注意してください。これにより、そのエンティティmultipart/related
に含まれる画像やその他の種類のメディアを参照できます。multipart/related
このプロパティは実際には単なる便利なプロパティであり、関連するコンテンツを適切に解釈できるように、MIME 構造を自分でトラバースする代わりにはなりません。