You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:03:03