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

C++ Windows程序如何提取OLE拖放的Outlook邮件内容与组件?

Outlook拖放OLE对象提取邮件内容问题解答

问题背景

用C++实现的Windows程序已支持接收MS Outlook拖放的OLE对象,能枚举拖放对象格式并读取TYMED_ISTREAM类型的流,但无法提取Outlook邮件的核心属性(正文、附件、发件人、收件人、主题等)。枚举到的FORMATETC记录中,格式代码49814的流读取到302字节,以下是具体问题及解决方案:


问题1:如何从读取的流中提取Outlook邮件?

具体操作步骤:

  1. 定位有效格式:你枚举到的RenPrivateItem(49708)、RenPrivateMessages(49735)、RenPrivateLatestMessages(49968)是Outlook私有剪贴板格式,对应流中存储的是MAPI对象的二进制序列化数据,这是提取邮件的关键来源。
  2. 初始化MAPI环境:调用MAPIInitialize(NULL)启动MAPI库,确保后续接口调用正常。
  3. 加载邮件对象:将读取到的流传入OpenIMsgOnIStg函数,该函数会从IStream中解析出Outlook邮件的IMessage接口实例。
  4. 提取邮件属性:通过IMessage接口的方法获取各属性:
    • 主题:调用GetProps获取PR_SUBJECT属性值
    • 发件人:获取PR_SENDER_NAME、PR_SENDER_EMAIL_ADDRESS属性
    • 收件人:调用GetRecipientTable获取收件人列表,再遍历表项提取信息
    • 正文:获取PR_BODY(纯文本)或PR_HTML(HTML格式)属性
    • 附件:调用GetAttachmentTable枚举附件,通过OpenAttach打开附件并读取内容
  5. 清理资源:使用完MAPI接口后,调用MAPIUninitialize()释放资源。

问题2:是否需要调用RegisterClipboardFormat?

2.1 应注册的格式名称

需要注册以下Outlook私有格式名称,获取对应的CLIPFORMAT值:

  • "RenPrivateItem"
  • "RenPrivateMessages"
  • "RenPrivateLatestMessages"

这些名称对应你枚举到的格式代码49708、49735、49968。

2.2 注册的必要性(枚举为何不够)

  • 枚举仅能拿到格式的数值代码,无法直接关联到格式的逻辑含义(比如哪个对应单邮件、哪个对应多邮件集合),注册后可通过名称明确指定目标格式,避免依赖不稳定的数值代码(不同系统/Outlook版本可能变动)。
  • 主动请求特定格式数据时,必须用注册得到的CLIPFORMAT构造FORMATETC,确保GetData能精准获取对应的数据。

可用格式说明

你枚举到的格式中,以下可用于获取Outlook邮件:

  • 49708 (RenPrivateItem):对应单封Outlook邮件对象
  • 49735 (RenPrivateMessages):对应多封邮件的集合
  • 49968 (RenPrivateLatestMessages):最新邮件的集合
  • 49395 (FileGroupDescriptor):仅当邮件以.eml文件形式拖放时有效,包含文件描述,但不是直接的邮件对象

枚举拖放对象/格式的代码(中文注释版)

HRESULT __stdcall MyDropTarget_CLASS::Drop(IDataObject* pDataObj, DWORD grfKeyState,
    POINTL pt,
    DWORD* pdwEffect) {

    bool ok_get_enum = false, ok_reset = false;

    DragWindowPtr->SetNormalBrush();

    IEnumFORMATETC* ppenumFormatEtc;
    HRESULT res = pDataObj->EnumFormatEtc(DATADIR_GET, &ppenumFormatEtc);
    ok_get_enum = (res == S_OK);
    if (!ok_get_enum)
        std::cout << "EnumFormatEtc 错误 = " << res << std::endl;

    if (ok_get_enum) {
        res = ppenumFormatEtc->Reset();
        ok_reset = (res == S_OK);
        if (!ok_reset)
            std::cout << "EnumFormatEtc.Reset 错误 = " << res << std::endl;
    }

    if (ok_reset) {

        std::cout << "------ 枚举格式开始 ---------" << std::endl;

        const ULONG ElementsInPortions = 5;     // 每次读取的记录数量
        FORMATETC ItemsArray[ElementsInPortions];
        ULONG FetchedNumber = 0;
        bool DoNext = true;
        while (DoNext) {
            DoNext = (res = ppenumFormatEtc->Next(ElementsInPortions, &(ItemsArray[0]), &FetchedNumber)) == S_OK;
            std::cout << "请求数量 = " << ElementsInPortions << "    获取数量 = " << FetchedNumber << std::endl;
            for (ULONG i = 0; i < FetchedNumber; i++) {

                CLIPFORMAT CurFormatCode = ItemsArray[i].cfFormat;
                DWORD CurAspect = ItemsArray[i].dwAspect;
                DWORD CurTymed = ItemsArray[i].tymed;
                LONG LIndex = ItemsArray[i].lindex;

                std::cout << "      格式 = " << CurFormatCode << "(" << GetFormatName(CurFormatCode) << ")" <<
                    "   显示方式 = " << CurAspect <<
                    "   存储介质 = " << CurTymed << "(" << GetTymedName(CurTymed) << ")" <<
                    "   索引 = " << LIndex << std::endl;

                if (CurTymed == TYMED_ISTREAM) {
                    std::cout << "--------- 读取流数据 ---------" << std::endl;
                    GetData_IStream(pDataObj, &(ItemsArray[i]));
                    std::cout << "-----------------------------------------" << std::endl << std::endl;
                }

            }

        }

        std::cout << "------ 枚举结束 ---------" << std::endl;

        ppenumFormatEtc->Release();
    }

    *pdwEffect = DROPEFFECT_COPY;

    return S_OK;
}

// 读取ISTREAM的代码
bool GetData_IStream(IDataObject* pDataObj, const FORMATETC* in_FormatEtc) {

    // ---------------- 查询是否支持该格式 ----------------
    FORMATETC ReadFormat;
    ZeroMemory(&ReadFormat, sizeof(ReadFormat));

    ReadFormat.cfFormat = in_FormatEtc->cfFormat;                     // 剪贴板格式
    ReadFormat.tymed = TYMED_ISTREAM;           // 存储介质类型
    ReadFormat.ptd = NULL;                      // 目标设备
    ReadFormat.dwAspect = DVASPECT_CONTENT;     // 内容显示方式
    ReadFormat.lindex = -1;                     // 索引

    HRESULT res = pDataObj->QueryGetData(&ReadFormat);
    bool ok_query = (res == S_OK);
    if (ok_query)
        std::cout << "QueryGetData - 成功" << std::endl;
    else {
        std::cout << "QueryGetData - 错误 = " << res << std::endl;
        return false;
    }

    // ---------------- 获取数据 ----------------
    ZeroMemory(&ReadFormat, sizeof(ReadFormat));

    ReadFormat.cfFormat = in_FormatEtc->cfFormat;                     // 剪贴板格式
    ReadFormat.tymed = TYMED_ISTREAM;           // 存储介质类型
    ReadFormat.ptd = NULL;                      // 目标设备
    ReadFormat.dwAspect = DVASPECT_CONTENT;     // 内容显示方式
    ReadFormat.lindex = -1;                     // 索引

    STGMEDIUM MediumInfo;   
    res = pDataObj->GetData(&ReadFormat, &MediumInfo);
    bool ok_getdata = (res == S_OK);
    if (ok_getdata)
        std::cout << "GetData - 成功" << std::endl;
    else {
        std::cout << "GetData - 错误 = " << res << std::endl;
        return false;
    }

    // 确认返回的存储介质类型正确
    if (MediumInfo.tymed != ReadFormat.tymed) {
        std::cout << "GetData 错误 - 无效的_MediumInfo.tymed_ = " << MediumInfo.tymed << std::endl;
        return false;
    }

    IStream* Stream = MediumInfo.pstm;
    if (Stream == NULL) {
        std::cout << "GetData 错误 - Stream == NULL." << std::endl;
        return false;
    }

    bool ReadResult = false;

    // ---------------- 分配缓冲区读取流 ----------------
    const int BufferSize = 100000;
    char* BufferPtr = (char*)malloc(BufferSize);
    if (BufferPtr != nullptr) {

        // ---------------- 读取流数据 ------------------------
        ULONG ActuallyRead = 0;     
        ULONG TotalRead = 0;        
        bool StreamEof = false;
        bool ReadError = false;

        // --- 定位到流开头 ---
        ULARGE_INTEGER NewPos = { 0, 0 };
        LARGE_INTEGER SetPos = { 0, 0 };
        HRESULT res_seek = Stream->Seek(SetPos, STREAM_SEEK_SET, &NewPos);
        ReadError = (res_seek != S_OK);

        if (ReadError) {
            bool pending = res_seek == E_PENDING;
            if (pending)
                std::cout << "Stream.Seek 等待中 :((" << std::endl;
            else
                std::cout << "Stream.Seek 错误 = " << res_seek << std::endl;
        }
        else
            std::cout << "Stream.Seek 成功. 新位置 = LO=" << NewPos.LowPart << "  HI=" << NewPos.HighPart << std::endl;

        while (!StreamEof && !ReadError) {
            HRESULT res_read = Stream->Read(BufferPtr, (ULONG)BufferSize, &ActuallyRead);

            switch (res_read) {
                case S_OK: 
                case S_FALSE: {
                    // --- 读取到数据 ---
                    StreamEof = (ActuallyRead < BufferSize);
                    TotalRead = TotalRead + ActuallyRead;
                    break;
                }  // S_OK, S_FALSE

                case E_PENDING: {
                    // 等待中... 暂不处理
                    break;
                } // E_PENDING

                default: {
                    ReadError = true;
                    std::cout << "Stream 读取错误 = " << res_read << std::endl;
                } // default
            } // switch
        } // while

        ReadResult = !ReadError;

        if (!ReadError) {
            std::cout << "Stream 读取成功. 总读取字节数 = " << TotalRead << std::endl;
        }
    }
    else {
        std::cout << "Malloc 错误 - 无法分配流数据缓冲区." << std::endl;
        ReadResult = false;
    }

    Stream->Release();
    return ReadResult;
}

内容的提问来源于stack exchange,提问作者LUN

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 10:35:54