Click or drag to resize
MimeKit

Working with messages

MimeKit provies a number of ways to get the data you want from a message.

This topic contains the following sections:

The Message Structure

A common misunderstanding about email is that there is a well-defined message body and then a list of attachments. This is not really the case. The reality is that MIME is a tree structure of content, much like a file system.

Luckily, MIME does define a set of general rules for how mail clients should interpret this tree structure of MIME parts. The Content-Disposition header is meant to provide hints to the receiving client as to which parts are meant to be displayed as part of the message body and which are meant to be interpreted as attachments.

The Content-Disposition header will generally have one of two values: inline or attachment.

The meaning of these values should be fairly obvious. If the value is attachment, then the content of said MIME part is meant to be presented as a file attachment separate from the core message. However, if the value is inline, then the content of that MIME part is meant to be displayed inline within the mail client's rendering of the core message body. If the Content-Disposition header does not exist, then it should be treated as if the value were inline.

Technically, every part that lacks a Content-Disposition header or that is marked as inline, then, is part of the core message body.

There's a bit more to it than that, though.

Modern MIME messages will often contain a multipart/alternative MIME container which will generally contain a text/plain and text/html version of the text that the sender wrote. The text/html version is typically formatted much closer to what the sender saw in his or her WYSIWYG editor than the text/plain version.

The reason for sending the message text in both formats is that not all mail clients are capable of displaying HTML.

The receiving client should only display one of the alternative views contained within the multipart/alternative container. Since alternative views are listed in order of least faithful to most faithful with what the sender saw in his or her WYSIWYG editor, the receiving client should walk over the list of alternative views starting at the end and working backwards until it finds a part that it is capable of displaying.

Example:

multipart/alternative
  text/plain
  text/html

As seen in the example above, the text/html part is listed last because it is the most faithful to what the sender saw in his or her WYSIWYG editor when writing the message.

To make matters even more complicated, sometimes modern mail clients will use a multipart/related MIME container instead of a simple text/html part in order to embed images and other multimedia content within the HTML.

Example:

multipart/alternative
  text/plain
  multipart/related
    text/html
    image/jpeg
    video/mp4
    image/png

In the example above, one of the alternative views is a multipart/related container which contains an HTML version of the message body that references the sibling video and images.

Now that you have a rough idea of how a message is structured and how to interpret various MIME entities, the next step is learning how to traverse the MIME tree using MimeKit.

Traversing a Message

The Body is the top-level MIME entity of the message. Generally, it will either be a TextPart or a Multipart.

There are 3 ways of iterating over the tree structure of a message using MimeKit. The first way is to recursively traverse the MIME structure like this:

C#
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
}

A second way, which is a bit cleaner and doesn't require recursive methods, is to use a MimeIterator:

C#
var attachments = new List<MimePart> ();
var multiparts = new List<Multipart> ();

using (var iter = new MimeIterator (message)) {
    // collect our list of attachments and their parent multiparts
    while (iter.MoveNext ()) {
        var multipart = iter.Parent as Multipart;
        var part = iter.Current as MimePart;

        if (multipart != null && part != null && part.IsAttachment) {
            // keep track of each attachment's parent multipart
            multiparts.Add (multipart);
            attachments.Add (part);
        }
    }
}

// now remove each attachment from its parent multipart...
for (int i = 0; i < attachments.Count; i++)
    multiparts[i].Remove (attachments[i]);

And finally, for those that prefer to use the visitor pattern, MimeKit includes the MimeVisitor class for visiting each node in the MIME tree structure. For example, the following MimeVisitor subclass could be used to generate HTML to be rendered by a browser control (such as WebBrowser):

C#
/// <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.Content.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);
    }
}

And the way you'd use this visitor might look something like this:

C#
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);
}
Using the TextBody and HtmlBody Properties

To simplify the common task of getting the text of a message, MimeKit includes two properties that can help you get the text/plain or text/html version of the message body. These are TextBody and HtmlBody, respectively.

Keep in mind, however, that at least with the HtmlBody property, it may be that the HTML part is a child of a multipart/related, allowing it to refer to images and other types of media that are also contained within that multipart/related entity. This property is really only a convenience property and is not a really good substitute for traversing the MIME structure yourself so that you may properly interpret related content.

Enumerating Body Parts

Sometimes traversing the body of a message is overkill when all you really need to do is something quick & dirty. For this reason, MimeKit provides the BodyParts and Attachments properties which flatten out the hierarchy and allow you to iterate over all of the body parts (or attachments) in depth-first order.

C#
foreach (var attachment in message.Attachments) {
    if (attachment is MessagePart) {
        var fileName = attachment.ContentDisposition?.FileName :
            (attachment.ContentType.Name ?? "attached.eml");
        var rfc822 = (MessagePart) attachment;

        rfc822.Message.WriteTo (stream);
    } else {
        var part = (MimePart) attachment;
        var fileName = part.FileName;

        using (var stream = File.Create (fileName))
            part.Content.DecodeTo (stream);
    }
}
Getting the Decoded Content of a MimePart

At some point, you're going to want to extract the decoded content of a MimePart (such as an image) and save it to disk or feed it to a UI control to display it.

Once you've found the MimePart object that you'd like to extract the content of, here's how you can save the decoded content to a file:

C#
// This will get the name of the file as specified by the sending mail client.
// Note: this value *may* be null, so you'll want to handle that case in your code.
var fileName = part.FileName;

using (var stream = File.Create (fileName)) {
    part.Content.DecodeTo (stream);
}

You can also get access to the original raw content by "opening" the Content. This might be useful if you want to pass the content off to a UI control that can do its own loading from a stream.

C#
using (var stream = part.Content.Open ()) {
    // At this point, you can now read from the stream as if it were the original,
    // raw content. Assuming you have an image UI control that could load from a
    // stream, you could do something like this:
    imageControl.Load (stream);
}

There are a number of useful filters that can be applied to a FilteredStream, so if you find this type of interface appealing, I would suggest taking a look at the available filters in the MimeKit.IO.Filters namespace or even write your own! The possibilities are limited only by your imagination.

See Also

Other Resources