Introduction
This article is going to focus on how to dissect a msg file generated by Outlook. It covers how to read the basic properties of the mail message, attachments and any msg attachments (these need to be handled differently).
Using the Code
The code is pretty simple to use. You construct a new instance of the OutlookStorage.Message
class, sending it the path to a msg file or a Stream
containing an IStorage
. The Stream
constructor is provided so that it is easy to integrate with the Outlook drag and drop code in another of my articles and this is shown in the demo application.
private static void main()
{
OutlookStorage.Message outlookMsg = new OutlookStorage.Message(@"C:\test.msg");
DisplayMessage(outlookMsg);
}
private static void DisplayMessage(OutlookStorage.Message outlookMsg)
{
Console.WriteLine("Subject: {0}", outlookMsg.Subject);
Console.WriteLine("Body: {0}", outlookMsg.BodyText);
Console.WriteLine("{0} Recipients", outlookMsg.Recipients.Count);
foreach (OutlookStorage.Recipient recip in outlookMsg.Recipients)
{
Console.WriteLine(" {0}:{1}", recip.Type, recip.Email);
}
Console.WriteLine("{0} Attachments", outlookMsg.Attachments.Count);
foreach (OutlookStorage.Attachment attach in outlookMsg.Attachments)
{
Console.WriteLine(" {0}, {1}b", attach.Filename, attach.Data.Length);
}
Console.WriteLine("{0} Messages", outlookMsg.Messages.Count);
foreach (OutlookStorage.Message subMessage in outlookMsg.Messages)
{
DisplayMessage(subMessage);
}
}
Save a Msg and All Attachments to the File System
This is an example on how to save a message and all associated attachments to the application path.
private static void main()
{
OutlookStorage.Message outlookMsg = new OutlookStorage.Message(@"C:\test.msg");
}
private static void SaveMessage(OutlookStorage.Message outlookMsg)
{
outlookMsg.Save(outlookMsg.Subject.Replace(":", ""));
foreach (OutlookStorage.Attachment attach in outlookMsg.Attachments)
{
byte[] attachBytes = attach.Data;
FileStream attachStream = File.Create(attach.Filename);
attachStream.Write(attachBytes, 0, attachBytes.Length);
attachStream.Close();
}
foreach (OutlookStorage.Message subMessage in outlookMsg.Messages)
{
SaveMessage(subMessage);
}
}
Understanding the Code
To read the msg file produced by Outlook, there are two concepts to understand. The first is that an msg file is logically a MAPI object with MAPI properties and the second is that phyiscally the MAPI object and its properties are stored in an IStorage. Microsoft has kindly provided a specification on how the MAPI properties are mapped to the IStorage, so at this point I will defer to that and just go over the catches that popped up when figuring out how to save a sub message out of its parent.
Saving a Sub Message
Saving a sub message out of the parent message has a few catches. The property stream header needs to be padded and the name to id mapping storage needs to be copied to the sub message storage.
Fixing the Property Stream
MAPI property values can be stored in a sub storage, a sub stream or in the case of fixed size values (like an integer) a special sub stream called the property stream. The property stream consists of a variable length header and then an array of 16 byte pairs with a property identifier in the first 8 bytes and the property value in the second 8.
It is the variable length header that you should take note of. It is 8 bytes for an attachment or recipient storage, 32 bytes for a top level msg and 24 bytes for a sub msg. This means that if you want to extract a sub message and save it without its parent you need to pad the end of the header with 8 null bytes.
The Name to Id Mapping
The other catch for saving a sub message is the name to id mapping storage which only exists on the top level msg, but contains the mappings for the entire tree. So when saving a sub message, this storage needs to be copied to it before saving for it to be valid.
Conclusion
Everything is encapsulated in the OutlookStorage.cs file as I don't like to release stuff with dependencies and prefer to just be able to drop a CS into my projects to get a particular piece of functionality. There is a region in there under a separate licence for the code to decompress the compressed RTF, but it is clearly marked.
History
- 8th July, 2010
- Fixed memory leak
- Fixed the "COM object that has been separated from its underlying RCW cannot be used" exception
- Added better determination of attachment file name
- 28th January, 2009: Original article