如何在C#中不使用Interop从邮件对象创建.msg文件
纯C#实现创建.msg文件(无需Outlook Interop)
我之前折腾过类似的需求,完全可以绕开Outlook Interop,直接基于OLE复合文件格式和MS-OXMSG规范来手动构建.msg文件。下面是一步步的实现思路和实用代码示例:
先搞懂.msg的本质
.msg文件其实是OLE复合文档(结构化存储),里面包含了多个嵌套的存储单元和流,每个流对应一个MAPI邮件属性(比如主题、发件人、正文等)。核心结构包括:
- 顶级存储下的
__nameid_version1.0流:用来映射命名属性(比如自定义字段) Message子存储:存储邮件的核心属性集合- 各种以属性ID+类型编码命名的流:比如
0037001F对应邮件主题(PR_SUBJECT)
步骤1:用Win32 API创建复合文件
.NET原生没有直接创建OLE复合文件的API,所以我们需要通过P/Invoke调用ole32.dll里的StgCreateStorageEx函数来初始化存储容器。
步骤2:添加邮件属性流
每个MAPI属性都有对应的属性ID和数据类型,你需要根据规范把这些属性转换成对应的流名称,然后写入数据。比如:
- PR_SUBJECT(主题):属性ID 0x0037,类型PT_STRING8 → 流名称
0037001F - PR_SENDER_NAME(发件人名称):属性ID 0x0C1A,类型PT_STRING8 → 流名称
0C1A001F
完整代码示例(基础版)
这个示例会创建一个包含主题、发件人和收件人的简单.msg文件:
using System; using System.Runtime.InteropServices; using System.Text; namespace MsgFileCreator { class Program { // GUID for the IStorage COM interface private static readonly Guid IID_IStorage = new Guid("{0000000B-0000-0000-C000-000000000046}"); [DllImport("ole32.dll", CharSet = CharSet.Unicode)] private static extern int StgCreateStorageEx( string filePath, uint accessMode, uint storageFormat, uint attributes, IntPtr options, IntPtr reserved, ref Guid interfaceId, out IntPtr storagePointer); // COM interface definitions for IStorage and IStream [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("0000000B-0000-0000-C000-000000000046")] private interface IStorage { void CreateStream([MarshalAs(UnmanagedType.LPWStr)] string streamName, uint mode, uint reserved1, uint reserved2, out IntPtr streamPointer); void OpenStream([MarshalAs(UnmanagedType.LPWStr)] string streamName, IntPtr reserved1, uint mode, uint reserved2, out IntPtr streamPointer); void CreateStorage([MarshalAs(UnmanagedType.LPWStr)] string storageName, uint mode, uint reserved1, uint reserved2, out IntPtr storagePointer); void OpenStorage([MarshalAs(UnmanagedType.LPWStr)] string storageName, IntPtr priorityStorage, uint mode, IntPtr excludeNames, uint reserved, out IntPtr storagePointer); void CopyTo(uint excludeCount, [MarshalAs(UnmanagedType.LPArray)] Guid[] excludeIds, IntPtr excludeNames, IntPtr destStorage); void MoveElementTo([MarshalAs(UnmanagedType.LPWStr)] string elementName, IntPtr destStorage, [MarshalAs(UnmanagedType.LPWStr)] string newName, uint flags); void Commit(uint commitFlags); void Revert(); void EnumElements(uint reserved1, IntPtr reserved2, uint reserved3, out IntPtr enumerator); void DestroyElement([MarshalAs(UnmanagedType.LPWStr)] string elementName); void RenameElement([MarshalAs(UnmanagedType.LPWStr)] string oldName, [MarshalAs(UnmanagedType.LPWStr)] string newName); void SetElementTimes([MarshalAs(UnmanagedType.LPWStr)] string elementName, IntPtr creationTime, IntPtr accessTime, IntPtr modifyTime); void SetClass(ref Guid classId); void SetStateBits(uint stateBits, uint mask); void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG stats, uint statFlags); } [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("0000000C-0000-0000-C000-000000000046")] private interface IStream { void Read([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] buffer, int count, out int bytesRead); void Write([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] buffer, int count, out int bytesWritten); void Seek(long offset, int origin, out long newPosition); void SetSize(long newSize); void CopyTo(IntPtr destStream, long count, out long bytesRead, out long bytesWritten); void Commit(int commitFlags); void Revert(); void LockRegion(long offset, long count, int lockType); void UnlockRegion(long offset, long count, int lockType); void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG stats, int statFlags); void Clone(out IntPtr clonedStream); } static void Main(string[] args) { string outputPath = @"C:\temp\MyTestMsg.msg"; // Initialize the compound file storage int result = StgCreateStorageEx( outputPath, 0x00001000 | 0x00000002 | 0x00000040, // STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE 0x00000000, // STGFMT_DOCFILE (OLE compound file) 0, IntPtr.Zero, IntPtr.Zero, ref IID_IStorage, out IntPtr rootStoragePtr); if (result != 0) throw new COMException("Failed to create root storage", result); IStorage rootStorage = (IStorage)Marshal.GetObjectForIUnknown(rootStoragePtr); try { // Create the required "Message" sub-storage rootStorage.CreateStorage("Message", 0x00001000 | 0x00000002 | 0x00000040, 0, 0, out IntPtr messageStoragePtr); IStorage messageStorage = (IStorage)Marshal.GetObjectForIUnknown(messageStoragePtr); try { // Add PR_SUBJECT stream (0037001F) AddStringPropertyToStorage(messageStorage, "0037001F", "Hello from Custom MSG Creator!"); // Add PR_SENDER_NAME stream (0C1A001F) AddStringPropertyToStorage(messageStorage, "0C1A001F", "Jane Smith"); // Add PR_TO_NAME stream (0E04001F) AddStringPropertyToStorage(messageStorage, "0E04001F", "John Doe"); messageStorage.Commit(0); Console.WriteLine($"MSG file created successfully at: {outputPath}"); } finally { Marshal.ReleaseComObject(messageStorage); } rootStorage.Commit(0); } finally { Marshal.ReleaseComObject(rootStorage); } } // Helper method to add a string property to a storage private static void AddStringPropertyToStorage(IStorage storage, string streamName, string value) { storage.CreateStream(streamName, 0x00001000 | 0x00000002 | 0x00000040, 0, 0, out IntPtr streamPtr); IStream stream = (IStream)Marshal.GetObjectForIUnknown(streamPtr); try { byte[] valueBytes = Encoding.ASCII.GetBytes(value); stream.Write(valueBytes, valueBytes.Length, out int bytesWritten); stream.Commit(0); } finally { Marshal.ReleaseComObject(stream); } } } }
进阶注意事项
- 属性类型对应:如果需要Unicode字符串(比如中文主题),要用
PT_UNICODE_STRING类型,对应流后缀001E,并且用Encoding.Unicode编码字节。 - 复杂内容处理:如果要添加附件、HTML正文或者收件人列表,需要创建对应的子存储(比如
Attachments子存储),这部分需要深入参考MS-OXMSG文档中关于附件和收件人章节的结构定义。 - 资源管理:一定要记得释放COM对象(
Marshal.ReleaseComObject),否则会造成内存泄漏。
偷懒的替代方案
如果不想手动实现所有细节,可以找一些开源的.msg处理库(基于MS-OXMSG规范的纯C#库),它们已经封装了复杂的结构处理,不需要你自己写所有底层代码。不过如果你坚持纯原生实现,上面的代码就是很好的起点。
内容的提问来源于stack exchange,提问作者Dinesh Kumar Lalchand
相关产品推荐
相关产品推荐

