Open the .msg with MsgFile.TryOpen, convert its message with MapiToMimeConverter, and write the result. The conversion follows Microsoft’s published mapping between Outlook properties and MIME (MS-OXCMAIL). It also returns a loss report naming everything that didn’t carry over. The program below converts a public test message from Apache POI.
dotnet new console -o MsgToEml && cd MsgToEml dotnet add package RecursiveAI.OneMail --version 0.3.0-preview.2 curl -LO https://raw.githubusercontent.com/apache/poi/REL_5_4_1/test-data/hsmf/attachment_test_msg.msg
Put this in Program.cs and run dotnet run:
using RecursiveAI.OneMail; using RecursiveAI.OneMail.Conversion; using RecursiveAI.OneMail.Conversion.OxcMail; using RecursiveAI.OneMail.Msg; if (!MsgFile.TryOpen("attachment_test_msg.msg", out MsgFile? msg, out var defects)) { Console.WriteLine($"Not an Outlook message: {string.Join(", ", defects)}"); return; } using (msg) { // MAPI to MIME by Microsoft's rules (MS-OXCMAIL); Convert never throws on content var result = new MapiToMimeConverter().Convert(msg.Message); using (FileStream eml = File.Create("attachment_test_msg.eml")) result.Value.WriteTo(eml); long size = new FileInfo("attachment_test_msg.eml").Length; Console.WriteLine($"Wrote attachment_test_msg.eml, {size:N0} bytes"); // Everything that did not carry over is in the loss report foreach (var group in result.Report.GroupBy(e => e.Kind)) Console.WriteLine($"{group.Key}: {group.Count()} properties"); } // Read it back as any mail client would IMailItem copy = Mail.Parse(File.ReadAllBytes("attachment_test_msg.eml")); Console.WriteLine($"{copy.Subject} | {copy.From[0]} | {copy.Sent?.Value:u}"); foreach (IMailAttachment a in copy.Attachments) Console.WriteLine($" {a.FileName} ({a.Size:N0} bytes)");
Wrote attachment_test_msg.eml, 35,184 bytes Approximated: 5 properties Internal: 46 properties Dropped: 46 properties test pièce jointe 1 | Nicolas1 23456 <[email protected]> | 2009-04-22 14:36:33Z test-unicode.doc (24,064 bytes) pj1.txt (89 bytes)
The .eml has the sender, recipient, date, subject, a plain text and an HTML body, and both attachments. The HTML was wrapped inside the message’s RTF body, and the converter unwraps it. Python’s email package and the onemail tool read the file too.
The loss report
result.Report lists every source property that wasn’t carried exactly, each with a Kind, a rule Code and a reason:
- Dropped here are mostly attachment bookkeeping that MIME has no field for (
NoInternetMapping), such as each attachment’s creation time. - Approximated are values carried in a weaker form: the
Dateheader holds whole seconds (DateSubSecondDropped), and the HTML came from the RTF body (HtmlFromRtf). - Internal are store bookkeeping such as entry IDs and search keys, listed so nothing is silent.
Nothing goes missing without an entry. The project sent 70 Outlook items through MIME and back and accounted for all 11,142 properties: 0 silent losses.
Limits
- A preview. 0.3.0-preview.2: the API can change before 1.0.
- Exchange addresses. In the project’s words: “EX addresses with no SMTP form in the item are left out and reported”. Each one is an
AddressUnresolvedentry in the report. - Bcc isn’t written by default. Set
new OxcMailExportOptions { IncludeBcc = true }to keep it. - One way for files. EML to MSG would need an MSG writer, which isn’t in this preview.
MimeToMapiConverterconverts to the MAPI model in memory.
For a whole mailbox, see Convert a PST file to EML. Every rule: MAPI and MIME conversion.