Mahindar Boregam
Mahindar Boregam

Reputation: 855

How to download the attachments of a email using Mailkit

I'm using the code from here to download attachments from mail using MailKit. In the foreach loop where attachments are retrieved, it is always returning empty. As it is empty it is not entering the foreach loop. Please correct me if i'm doing anything wrong.

        var messages = client.Inbox.Fetch(0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId);
        int unnamed = 0;

        foreach (var message in messages)
        {
            var multipart = message.Body as BodyPartMultipart;
            var basic = message.Body as BodyPartBasic;

            if (multipart != null)
            {

                //Here the attachments is always empty even though my mail has email with attachements
                var attachments = multipart.BodyParts.OfType<BodyPartBasic>().Where(x => x.IsAttachment);
                foreach (var attachment in attachments)
                {
                    var mime = (MimePart)client.Inbox.GetBodyPart(message.UniqueId.Value, attachment);
                    var fileName = mime.FileName;

                    if (string.IsNullOrEmpty(fileName))
                        fileName = string.Format("unnamed-{0}", ++unnamed);

                    using (var stream = File.Create(fileName))
                        mime.ContentObject.DecodeTo(stream);
                }
            }
            else if (basic != null && basic.IsAttachment)
            {
                var mime = (MimePart)client.Inbox.GetBodyPart(message.UniqueId.Value, basic);
                var fileName = mime.FileName;

                if (string.IsNullOrEmpty(fileName))
                    fileName = string.Format("unnamed-{0}", ++unnamed);

                using (var stream = File.Create(fileName))
                    mime.ContentObject.DecodeTo(stream);
            }
        }

Upvotes: 1

Views: 6474

Answers (2)

Matt K
Matt K

Reputation: 478

Here's what I'm using to traverse the MIME tree structure to get all possible attachments out. Note that at least in the case of the emails I was testing, the IsAttachment flag was never set to true.

First, make a recursive function that retrieves all basic body parts (which are the parts of the MIME message that contains the actual content of the message) as an IEnumerable<BodyPartBasic>:

private static IEnumerable<BodyPartBasic> GetBasicBodyParts(this BodyPart part)
{
    var multipart = part as BodyPartMultipart;
    var basic = part as BodyPartBasic;
    if (multipart != null)
    {
        foreach (var subPart in multipart.BodyParts)
        {
            if (subPart is BodyPartBasic)
            {
                yield return subPart as BodyPartBasic;
            }
            else
            {
                foreach (var subSubPart in subPart.GetBasicBodyParts())
                {
                    yield return subSubPart;
                }
            }
        }
    } else if (basic != null)
    {
        yield return basic;
    }
    else
    {
        yield break;
    }
}

And then put that function to use within a "download" function that is very similar to what @jstedfast has provided in other posts:

public static IEnumerable<FileInfo> DownloadAttachments(this EmailClient client, IMessageSummary message,
    string destination)
{
    fileNameFilter = fileNameFilter ?? AlwaysTrue;
    if (!Directory.Exists(destination))
        Directory.CreateDirectory(destination);

    var folder = client.Inbox;
    folder.Open(FolderAccess.ReadOnly);
    foreach (var part in message.Body.GetBasicBodyParts())
    {

        var mimeHeader = (MimePart) folder.GetBodyPart(message.UniqueId.Value, part, headersOnly: true);
        var headerFileName = mimeHeader.FileName;
        if (string.IsNullOrWhiteSpace(headerFileName))
            continue;


        var mime = (MimePart) folder.GetBodyPart(message.UniqueId.Value, part);
        var fileName = mime.FileName;

        var filePath = Path.Combine(destination, fileName);
        using (var stream = File.Create(filePath))
            mime.ContentObject.DecodeTo(stream);

        yield return new FileInfo(filePath);
    }
}

Note that this function:

  • Skips files that haven't a filename. If you want to keep those files, you could add in the unnamed counter from @jstedfast's other examples. You could also add in other logic to filter out files based on filename or other information you'd get in the MIME header.
  • First downloads only the header of the attachment. This allows for filename filtering without having to download the content first.
  • Is an extension method and can thus be used like: client.DownloadAttachments(someMessage, @"C:\tmp\attachments")
  • Returns System.IO.FileInfo objects for each attachment saved. This was useful for me but may not be relevant to others. It's not strictly necessary.

I hope this helps other folks.

Upvotes: 2

jstedfast
jstedfast

Reputation: 38618

The first thing to note is that MIME is a tree structure and not a flat list. A BodyPartMultipart can contain other BodyPartMultipart children as well as BodyPartBasic children, so when you come across another BodyPartMultipart, you need to descend into that as well.

My guess is that the messages you are examining have nested multiparts and that is why you are having trouble.

Either that or the "attachments" do not have a Content-Disposition header with a value of "attachment".

Upvotes: 1

Related Questions