Messages and conversion

Two levels

Level Types Use
Message MapiMessage, MapiRecipient, MapiAttachment Create, edit, save and reload messages. Subject, bodies, sender, recipients, attachments, embedded messages and arbitrary MAPI properties.
MSG structure MsgReader, MsgWriter, MsgDocument The property streams and the recipient and attachment storages of a .msg, for inspection or repair.
Compound file CfbReader, CfbWriter, CfbDocument Any OLE compound file: walk storages and streams, build and write new ones.

Loading

using XRay.Email.Msg;

var fromMsg = MapiMessage.FromFile("sample.msg");             // MSG
var fromEml = MapiMessage.LoadFromEml("incoming.eml");        // EML
var either  = MapiMessage.Load("unknown.bin", token);         // MSG or EML, detected from the bytes

Console.WriteLine(either.SourceFormat);                       // Msg, Eml or Created

FromFile and a seekable FromStream read an MSG on demand rather than copying it. MapiMessage.Load(Stream | path) detects the format from the bytes. EML parsing never throws: the MIME reader handles multipart structures, nested message/rfc822 parts, quoted-printable and base64, RFC 2047 encoded words in any charset the platform provides, raw UTF-8 headers (RFC 6532), RFC 2231 parameters, address groups and comments, and obsolete date syntax. Damaged input degrades instead of failing: missing closing boundaries, truncated base64, broken header folding.

Creating and converting

using XRay.Email.Msg;

var message = MapiMessage.Create("Quarterly report", "Please find the report attached.");
message.SenderName = "Alice";
message.SenderEmailAddress = "alice@example.com";
message.AddRecipient("bob@example.com", "Bob");
message.AddRecipient("carol@example.com", "Carol", MapiMessage.RecipientTypeCc);
message.AddAttachment("report.pdf", File.ReadAllBytes("report.pdf"), "application/pdf");

message.Save("report.msg");
message.SaveToEml("report.eml");

// Convert an EML file to MSG.
MapiMessage.LoadFromEml("message.eml").Save("message.msg");

SaveToEml writes any message as MIME with non-ASCII headers and file names encoded. Attachments are written base64, text included, so bytes stay exact. MIME boundaries carry a random token, so compare EML output structurally and not byte for byte.

MSG writing follows MS-OXMSG. String properties are stored in the message's code page (PR_MESSAGE_CODEPAGE, then PR_INTERNET_CPID, else Windows-1252).

Exchange addresses

Outlook often stores the sender and recipients of internal mail as X.500 names rather than SMTP addresses. MapiMessage.SenderSmtpAddress and MapiRecipient.SmtpAddress resolve the SMTP address where the message carries one.

Envelope helpers

Typed access to the properties a mail UI shows:

using XRay.Email.Msg;

var message = MapiMessage.Load(path, token);

MapiMessageFlags? flags = message.MessageFlags;         // PR_MESSAGE_FLAGS
bool read   = message.IsRead;
bool draft  = message.IsDraft;                          // MSGFLAG_UNSENT
MapiImportance? importance   = message.Importance;      // Low, Normal, High
MapiSensitivity? sensitivity = message.Sensitivity;

DateTime? sent     = message.SentTime;                  // client submit time, else submit time, else delivery time (UTC)
DateTime? created  = message.CreationTime;
DateTime? modified = message.LastModificationTime;

// Any other property, by id.
string? subject = message.GetPropertyString((ushort)CommonMessagePropertyId.Subject);
int? flagsValue = message.GetPropertyInt32((ushort)CommonMessagePropertyId.MessageFlags);
bool has        = message.HasProperty((ushort)CommonMessagePropertyId.InternetMessageId);

The other typed readers are GetPropertyBoolean, GetPropertyDateTime, GetPropertyBytes and the generic GetPropertyValue<T>. Calendar and contact properties have no dedicated API; they are reachable through these.

Compound files

CfbReader and CfbWriter read and write compound files (versions 3 and 4) independently of MSG:

using XRay.Email.Cfb;

using var cfb = CfbReader.FromFile("message.msg");
foreach (var (depth, entry) in cfb.IterTree())
{
    Console.WriteLine($"{new string(' ', depth * 2)}{entry.Name} ({entry.StreamSize} bytes)");
}

Damaged files

Malformed MSG input raises CfbException or MsgException, or is read leniently with an entry in MapiMessage.ValidationIssues (trailing bytes in a property stream, counts that do not match, a missing __nameid storage). Pass strict: true to FromFile or FromStream to reject those instead. No other exception type escapes loading.