Recursive AI

Convert MSG to EML in C#

All onemail recipes

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.

Terminal
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:

C#
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)");
Text
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 Date header 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 AddressUnresolved entry 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. MimeToMapiConverter converts to the MAPI model in memory.

For a whole mailbox, see Convert a PST file to EML. Every rule: MAPI and MIME conversion.