Strip attachments from emails using MailKit / MimeKit
Asked Answered
S

3

8

I'm using MailKit library to handle emails, which has been working well. However, I'm trying to split emails into their constituent files a) Main email (no attachments) b) Individual attachment files, to store on the filesystem.

I can save the attachments individually, but can't seem to remove them from the email body code. I.e. they're getting saved along with the main email, so duplicating data. :/

I've tried:

foreach (MimePart part in inMessage.BodyParts)
{ 
    if (part.IsAttachment)
    {
        // Remove MimePart    < This function isn't available on the collection.
    }
}

Have also tried:

var builder = new BodyBuilder();
foreach (MimePart part in inMessage.BodyParts)
{ 
    if (!part.IsAttachment)
    {
        // Add MimeParts to collection    < This function isn't available on the collection.
    }
}
outMessage.Body = builder.ToMessageBody();

If anyone can help with this, I'd much appreciate it.

Solution implemented FYI:

private string GetMimeMessageOnly(string outDirPath)
        {
            MimeMessage message = (Master as fsEmail).GetMimeMessage();

            if (message.Attachments.Any())
            {
                var multipart = message.Body as Multipart;
                if (multipart != null)
                {
                    while (message.Attachments.Count() > 0)
                    {
                        multipart.Remove(message.Attachments.ElementAt(0));
                    }
                }
                message.Body = multipart;
            }

            string filePath = outDirPath + Guid.NewGuid().ToString() + ".eml";
            Directory.CreateDirectory(Path.GetDirectoryName(outDirPath));
            using (var cancel = new System.Threading.CancellationTokenSource())
            {    
                using (var stream = File.Create(filePath)) 
                {
                    message.WriteTo(stream, cancel.Token);
                }
            }
            return filePath;
        }

And to get the attachments only:

private List<string> GetAttachments(string outDirPath)
        {
            MimeMessage message = (Master as fsEmail).GetMimeMessage();

            List<string> list = new List<string>();
            foreach (MimePart attachment in message.Attachments)
            {
                using (var cancel = new System.Threading.CancellationTokenSource())
                {
                    string filePath = outDirPath + Guid.NewGuid().ToString() + Path.GetExtension(attachment.FileName);
                    using (var stream = File.Create(filePath))
                    {
                        attachment.ContentObject.DecodeTo(stream, cancel.Token);
                        list.Add(filePath);
                    }
                }
            }
            return list;
        }
Stomatology answered 11/6, 2014 at 19:15 Comment(3)
this could be useful limilabs.com/blog/download-email-attachments-netTropopause
Thanks, but this link is based on Mail.dll and I'd like to stick with MailKit ideally.Stomatology
FWIW, you don't need to create a cancellation token unless you plan to be able to cancel saving the attachment to disk. You can either use CancellationToken.None or just not pass a cancellation token at all.Anfractuosity
H
10

You could retrieve all MimeParts that are attachments https://github.com/jstedfast/MimeKit/blob/master/MimeKit/MimeMessage.cs#L734 and then iterate over the all Multiparts and call https://github.com/jstedfast/MimeKit/blob/master/MimeKit/Multipart.cs#L468 for the attachments to remove.

The sample below makes a few assumptions about the mail e.g. there is only one Multipart some email client (Outlook) are very creative how mails are crafted.

static void Main(string[] args)
{
    var mimeMessage = MimeMessage.Load(@"x:\sample.eml");
    var attachments = mimeMessage.Attachments.ToList();
    if (attachments.Any())
    {
        // Only multipart mails can have attachments
        var multipart = mimeMessage.Body as Multipart;
        if (multipart != null)
        {
            foreach(var attachment in attachments)
            {
                multipart.Remove(attachment);
            }
        }
        mimeMessage.Body = multipart;
    }
    mimeMessage.WriteTo(new FileStream(@"x:\stripped.eml", FileMode.CreateNew));
}
Honorary answered 11/6, 2014 at 19:48 Comment(1)
Ah, this could be why Add and Remove methods weren't showing for me in the way I'd expect for a collection. I think I'd cast to MimeEntity; Maybe Multipart is what I need. I'll try this out tomorrow.Stomatology
A
7

Starting with MimeKit 0.38.0.0, you'll be able to use a MimeIterator to traverse the MIME tree structure to collect a list of attachments that you'd like to remove (and remove them). To do this, your code would look something like this:

var attachments = new List<MimePart> ();
var multiparts = new List<Multipart> ();
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]);
Anfractuosity answered 14/6, 2014 at 23:15 Comment(0)
M
0

I created an application, that downloads emails and attachments as well using Mailkit. I faced one problem: E-Mails sent from iOS with attached pictures were not processed correctly. MailKit did not add the images to the Attachments list.

I used this method to get only the text of the message:

private static string GetPlainTextFromMessageBody(MimeMessage message)
    {
        //content type needs to match text/plain otherwise i would store html into DB
        var mimeParts = message.BodyParts.Where(bp => bp.IsAttachment == false && bp.ContentType.Matches("text", "plain"));
        foreach (var mimePart in mimeParts)
        {
            if (mimePart.GetType() == typeof(TextPart))
            {
                var textPart = (TextPart)mimePart;
                return textPart.Text;
            }
        }
        return String.Empty;
    }

This is the method I used to download only the .jpg files:

foreach (var attachment in message.BodyParts.Where(bp => !string.IsNullOrEmpty(bp.FileName)))
{
    if (attachment.FileName.ToLowerInvariant().EndsWith(".jpg"))
    {
        //do something with the image here
    }
}
Measure answered 11/6, 2014 at 21:26 Comment(4)
How does this deal with HTML vs Plain Text? I ask because MailKit is doing a good job for me, when I take do MimeMessage.WriteTo(file) with a .eml extension. Those files get rendered well in Outlook as a viewer, and I'd like to retain that, but just remove the attachments from the MimeTree prior to saving out the .eml.Stomatology
ok, seems like I did not full get your requirement at first. I did not store the .eml but I stored the text in a database - and I just wanted to get the plain text for that. So Andreas' Answer seems to be more what you are looking for. But as I mentioned: try sending pictures from an iOS device and see if it will be removed from the .eml as you intended. Maybe then the second code snippet might help you in removing those images as well.Measure
FWIW, MimeKit only lists parts that have a Content-Disposition header with a value of "attachment" (+ any params) in the MimeMessage.Attachments property. This is because any other semantic is non-standard. iOS probably doesn't have a Content-Disposition header or it sets the value to "inline" or something.Anfractuosity
good explanation why the attachments are not in the list of attachments.Measure

© 2022 - 2024 — McMap. All rights reserved.