65.9K
CodeProject 正在变化。 阅读更多。
Home

Pocket PC 和 SmartPhone 99% .NET MAPI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (32投票s)

2007年10月31日

BSD

9分钟阅读

viewsIcon

371326

downloadIcon

2466

一个用于在 Pocket PC 和 SmartPhone 上使用 MAPI 的封装库,99% 用 .NET 编写,从而实现了一个快速且易于维护的库。

Screenshot - MAPIdotnet1.jpg

引言

我对 MAPI 的初步认识是想制作一个简单的应用程序,用于整理 PocketPC 手机上的收件箱和已发送邮件文件夹。我还想显示具有共同发件人和收件人的邮件,这类似于 Gmail 聊天记录、MSN 聊天记录等。C# 是显而易见的语言选择,因为它使得在 PocketPC(事实上,在所有平台上)上编码都非常简单。本文是系列文章的第一篇,记录了几个项目,这些项目共同构成了一个庞大的解决方案,旨在让该应用程序基本上取代 PocketPC 和 SmartPhone 自带的标准邮件查看程序。本文提供了并记录了 MAPI 的 .NET 封装库,解释了其 99% .NET 形式的由来和原因。

对于那些一直在研究将 MAPI 用于 PocketPC 项目的人来说,您可能会看到周围有其他几个项目。其中,我广泛地试验了其中的几个,这也有助于我在学习 MAPI 的过程中更深入地理解 MAPI。然而,最终,它们主要因为两个原因令我失望:它们通常无法开箱即用(这只是一个小问题),最重要的是,它们总是高度面向 C++。

高度面向 C++ 是一个缺点,这意味着 MAPI 库的任何扩展都必须用 C 来完成。事实证明,与我最终的 99% .NET 产品相比,这带来了性能损失。我将性能损失归因于 C++ 库主要旨在从 C++ 项目(这很有道理)中使用,而 .NET 封装库则是在此之上的另一个抽象。

库概述

可用的消息技术

在 Win32(PC 和 PocketPC)中,微软主要使用两种数据模型来存储有关消息、联系人、任务、约会等(Outlook 项)的信息:Pocket Outlook 对象模型 (POOM) 和消息应用程序编程接口 (MAPI)。后者仅支持消息。

POOM 是两者中较“现代”的,而 MAPI 已经存在多年。Pocket 设备(PocketPC、Smartphone 等)多年来一直支持 MAPI 的一个子集,而 POOM 才刚刚开始普及。

选择消息技术

我雄心勃勃的项目的第一部分是能够在我的手机上用 C# 访问邮件。所以,从零开始,我做了我第一次在 .NET 中做任何事情时都会做的事情:我直接查看类库。当我看到 Microsoft.WindowsMobile.PocketOutlook 命名空间时,我认为我找到了宝藏,直到我仔细一看。这个命名空间是微软的 POOM 对象 .NET 封装库。虽然它是一个功能强大的小型命名空间,用于访问联系人、任务、约会等,但它 **不支持** 消息。所以,POOM 被排除在外了。

因此,我不得不诉诸于查看 Pocket 设备的原生(Win32)API 来访问消息,这让我发现了 MAPI。对 MAPI 进行快速研究让我意识到我找到了正确的东西:MsgStores(消息存储)、Folders(文件夹)和 Messages(消息)。

我的第一个实现——我只打算简要提及——与其他人的实现类似,即有一个大型 C++ 库。这导致了一个大型 C++ 库和一个大型 C# 封装库。随着项目的增长和所需功能的增加,.NET 封装函数的数量远远超出了需要。该库运行速度也很慢,因为大量的字符串在 C++ 和 C# 之间进行封送。更糟糕的是,长时间使用后,出现了明显的内存泄漏,这迫使我必须定期关闭程序。

整个项目被放弃,转而采用更受青睐且显而易见的方法:在 .NET 世界中完成所有工作,包括创建和销毁对象以及将数据封送到 .NET。

MAPI 的工作原理

Win32 和 Pocket 设备上的 MAPI 提供了一些数据库的封装函数,这些数据库在后台存储了所有 MAPI 项。有趣的是,在 PocketPC 设备上,邮件仍然存储在旧的 CE 数据库中(我现在记不起名字了……),据说很快就会迁移到 CE SQL 数据库。由于有数据库后端,MAPI 具有数据库的风格,这意味着要访问给定对象的 {},需要先填充并排序表列。然后请求所需的行数。要真正深入理解 MAPI 的工作原理,我建议查看 API,它相当令人困惑,除非您考虑到它是一个数据库封装库。

在 .NET 中完成所有工作

决定在 C# (.NET) 中完成所有工作,但仍然需要与 MAPI 进行低级原生交互。事实证明,MAPI 只有少量单独的函数(例如,启动第一个事务 MAPILogonEx,它会获取一个 IMAPISession)并且为每个消息项提供了接口,如 MsgStoreFolderMessage 等。在耗尽了尝试从 C# 调用原生接口函数后,我不得不承认我无法创建一个 100% .NET MAPI 封装库。结果是创建一个 C++ 封装库,该库封装了 MAPI 接口的各个成员函数。例如,对于 IMAPISession 成员函数 HRESULT GetMsgStoresTable(ULONG ulFlags, IMAPITable ** lppTable),会生成以下函数

HRESULT IMAPISessionGetMsgStoresTable(IMAPISession *pSession,
        IMAPITable ** lppTable)
{
    return pSession->GetMsgStoresTable(0, lppTable);
}

MAPIdotnet 库结构

C# MAPI 库有两个抽象级别

内部 MAPI 封装类

对于每个原生 MAPI 接口,都有一个相应的 .NET 接口和类,负责持有和释放原生 MAPI 指针。每当返回原生接口时(例如,打开一个文件夹返回一个 IMAPIFolder 指针,或者请求一个 MAPI 数据表),就会创建一个相应的 C# 封装类,它持有该指针以确保它不会丢失。当 C# 封装类失去其引用(准备进行垃圾回收)时,相应的 MAPI 指针将被释放,以确保没有内存泄漏。这些封装类和接口位于 MAPIdotnet.cemapi 命名空间中。以下代码显示了所有项的基础接口 IMAPIUnknown 如何跟踪接口指针

public interface IMAPIUnknown
{
    void Release();

    IntPtr Ptr { get; }
}
private abstract class MAPIUnknown : IMAPIUnknown
{
    [DllImport("MAPILib.dll", EntryPoint = "Release")]
    public static extern uint pRelease(IntPtr iUnknown);

    protected IntPtr ptr = IntPtr.Zero;

    public void Release()
    {
        if (this.ptr != IntPtr.Zero)
        {
            pRelease(this.ptr);
            this.ptr = IntPtr.Zero;
        }
    }

    public IntPtr Ptr { get { return this.ptr; } }

    ~MAPIUnknown() { Release(); }
}

在从项获取属性数据时,也使用了相同的严格指针控制。像所有 Win32 微软 API 一样,MAPI 大量使用结构来获取和设置属性。而不是信任 .NET Marshaller 来封送整个结构(当结构的内容变得复杂时,我遇到过问题),而是直接使用指针,并进行单独的封送调用,以便在 C# 中重新组装结构。例如,一个常见的 MAPI 结构是 SPropValue 结构,其形式为

struct {
    ULONG ulPropTag;
    ULONG dwAlignPad;
    union _PV Value;
} SPropValue, *LPSPropValue;

在获取和设置对象(例如,邮件、文件夹等)的属性时,您会收到或传入一个 SPropValue 数组。因此,而不是信任 C# Marshaller,直接使用指针。对于结构数组,首先封送第一个结构,然后递增指针,依此类推。最后,根据 MAPI 的规定释放指针。下面提供了一个 cemapi.MAPIProp 封装类的代码片段作为示例

public IPropValue[] GetProps(PropTags[] tags)
{
    // Populate the tags
    uint[] t = new uint[tags.Length + 1];
    t[0] = (uint)tags.Length;
    for (int i = 0; i < tags.Length; i++)
        t[i + 1] = (uint)tags[i];

    IntPtr propVals = IntPtr.Zero;
    uint count = 0;

    // Call the native MAPI wrapper interface member wrapper:
    HRESULT hr = pIMAPIPropGetProps(this.ptr, t, out count, ref propVals);
    if (hr != HRESULT.S_OK)
        throw new Exception("GetProps failed: " + hr.ToString());

    IPropValue[] props = new IPropValue[count];
    uint pProps = (uint)propVals;

    // Iterate over the received property array, using the pointer offset
    for (int i = 0; i < count; i++)
    {
        pSPropValue lpProp =
            (pSPropValue)Marshal.PtrToStructure((IntPtr)(
            pProps + i * cemapi.SizeOfSPropValue), typeof(pSPropValue));
        props[i] = new SPropValue(lpProp);
    }
    // Free the pointer
    cemapi.MAPIFreeBuffer(propVals);
    return props;
}

公开的接口

该库公开了一个单独的 MAPI 类以及一系列与 MAPI 项对应的接口。在大多数情况下,每个公开接口的实现都有一个对应的 cemapi 封装接口。以下列表不言自明

  • MAPI
  • IMAPIEntryID
  • IMAPIContact
  • IMAPIFolderID
  • IMAPIFolder
  • IMAPIMessageID
  • IMAPIMessage
  • IMAPIMsgStore
  • IMAPIProp

FolderMessageMsgStore 都继承自 PropProp 公开通用属性,如 DisplayName(显示名称)、EntryID(用于比较对象的会话特定 ID)和显示 Icon(尽管在我看来,至少 SMS 消息没有图标)。IMAPIMessageIDIMAPIFolderID 扩展自 IEntryID,其中包含消息和文件夹的特定 ID。然后可以使用 ID 来打开文件夹和消息。每个公开接口的实现都提供了更“人性化”的方式来获取和设置信息。例如,而不是进行数据库请求来获取邮件主题或时间,IMAPIMessage 接口具有 string Subject { get; }DateTime LocalDeliveryTime { get; } 属性,它们会进行所有必要的转换。

Using the Code

此处提供的解决方案包含三个项目:最小化的 C++ MAPI 封装库(MAPIlib)、C# MAPI 封装库(MAPIdotnet)以及一个用于 PocketPC 和 SmartPhones 的示例项目,用于显示消息及其部分属性(PocketMail)。

Screenshot - MAPIdotnet2.jpg

如果在尝试访问原生 Win32 MAPIlib.dll 时出现异常,可能是因为它在部署解决方案时没有被复制到目录中。要执行此操作,请将“现有项”添加到 PocketMail 项目中,并导航到 MAPIlib/bin/[Release 或 Debug]/MAPIlib.dll。 **不要** 点击“打开”。点击“打开”按钮 **旁边** 的小下拉箭头,然后选择“另存为链接”。然后,在该文件的属性框中,选择“生成操作 = 内容”和“复制到输出目录 = 始终”。

警告:我目前正在对发生项目事件(例如,新邮件到达、邮件被删除、邮件主题被更改等)进行“优雅化”处理,因此建议忽略 cemapi.IMAPIAdviseSink

消息存储事件

IMAPIMsgStore 现在可以注册事件,例如新邮件到达、消息/文件夹更改/移动/复制/删除。IMAPIMsgStore 公开三个事件。除了订阅文件夹和消息的公开事件外,还必须通过设置 EventNotifyMask 来屏蔽不同的事件类型。一个小提示,您会很快意识到,注册所有可能的事件是一场噩梦,因为每次发生某事时都会引发大约六个事件!

关注点

性能

在我编写库的过程中,我一直担心几乎所有东西都用 C# 实现会意味着它运行起来会像狗一样。事实证明,我错了。在我的手机上,我的收件箱中有 1500 多封邮件,获取其中每一封(这需要为每一封创建至少两个类并获取它们的所有信息)只需要一秒多一点。相比之下,构建 PocketMail 程序提供的节点树(甚至还没有显示)大约需要十秒钟。然后,将填充的节点添加到 TreeViewTreeView.Nodes.AddRange(TreeNode[] nodes))需要超过三十秒!

持续开发

MAPIdotnet 现在在 SourceForge 上有一个专门的项目,链接在此处。项目页面提供了对 SVN 存储库的访问,其中包含最新的添加和新项目。如果您也有兴趣,请随时与我联系,成为一名具有提交权限的开发者。

历史

  • 2007 年 10 月 31 日 - MAPIdotnet 的初始发布。
  • 2008 年 2 月 13 日 - 更新,包括添加了 MessageFolder ID,删除消息,撰写新消息,以及消息/文件夹/存储事件。
  • 2008 年 4 月 28 日 - 添加了关于 SourceForge 项目详细信息的说明。
© . All rights reserved.