Open the file with Mail.Open, and for each message copy attachment.Open() to a file. The same loop works for .eml, Outlook .msg, winmail.dat, PST stores and mbox files. The program below saves the attachments of three public test files from Apache Tika and Apache POI.
dotnet new console -o ExtractAttachments && cd ExtractAttachments dotnet add package RecursiveAI.OneMail --version 0.2.0-preview.2 curl -LO https://raw.githubusercontent.com/apache/tika/4.2.0/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testRFC822_multipart_attachments.eml curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hsmf/attachment_test_msg.msg curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hmef/quick-winmail.dat
Put this in Program.cs and run dotnet run (or dotnet run -- yourfile.eml):
using RecursiveAI.OneMail; // Any message file or mailbox works the same: .eml, .msg, winmail.dat, .pst, mbox string[] inputs = args.Length > 0 ? args : [ "testRFC822_multipart_attachments.eml", "attachment_test_msg.msg", "quick-winmail.dat", ]; foreach (string input in inputs) { string dir = Path.Combine("attachments", Path.GetFileNameWithoutExtension(input)); Directory.CreateDirectory(dir); using MailContainer mail = Mail.Open(input); foreach (IMailItem msg in mail.Enumerate()) { foreach (IMailAttachment a in msg.Attachments) { // FileName is what the sender wrote: keep only the name, never a path string name = Path.GetFileName(a.FileName ?? ""); if (name == "") name = "attachment.bin"; string path = Path.Combine(dir, name); for (int i = 2; File.Exists(path); i++) path = Path.Combine(dir, $"{i}-{name}"); using (Stream content = a.Open()) // decoded content using (FileStream file = File.Create(path)) content.CopyTo(file); Console.WriteLine($"{path} {new FileInfo(path).Length:N0} bytes"); } } }
attachments/testRFC822_multipart_attachments/Test TxtA.txt 33 bytes attachments/testRFC822_multipart_attachments/Test TxtB.txt 33 bytes attachments/attachment_test_msg/test-unicode.doc 24,064 bytes attachments/attachment_test_msg/pj1.txt 89 bytes attachments/quick-winmail/quick.doc 19,968 bytes attachments/quick-winmail/quick.html 428 bytes attachments/quick-winmail/quick.pdf 18,638 bytes attachments/quick-winmail/quick.txt 235 bytes attachments/quick-winmail/quick.xml 143 bytes
Open() returns the decoded content: base64 and quoted-printable are undone, and Outlook attachments come straight from the file. The five files from quick-winmail.dat are byte-identical to the copies Apache POI keeps beside it.
Things to know
- Never use the name as a path.
FileNameis what the sender wrote, and it can hold../or a full path.Path.GetFileNamekeeps only the name; two attachments with the same name don’t overwrite each other. - Inline images (pictures the HTML body shows through
cid:links) are attachments too, withIsInlineset. Skip them withif (a.IsInline) continue;. - Attached emails have
Embeddedset: read thatIMailItemthe same way to reach the files inside it. - winmail.dat inside an email is unpacked for you: the message lists the files in it instead.
Limits
- A preview. 0.2.0-preview.2: the API can change before 1.0.
Sizecosts a pass. It decodes the content once to measure it, so prefer the file length after copying, as above.- Content type from Outlook. MSG and TNEF attachments that don’t store a MIME type report
application/octet-stream.
Reading the rest of a message: Parse an EML file, Read an Outlook MSG file. The shared view: Mail.Open and the read view.