Pocket PC 和 SmartPhone 99% .NET MAPI
一个用于在 Pocket PC 和 SmartPhone 上使用 MAPI 的封装库,99% 用 .NET 编写,从而实现了一个快速且易于维护的库。
引言
我对 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
)并且为每个消息项提供了接口,如 MsgStore
、Folder
、Message
等。在耗尽了尝试从 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
Folder
、Message
和 MsgStore
都继承自 Prop
。Prop
公开通用属性,如 DisplayName
(显示名称)、EntryID
(用于比较对象的会话特定 ID)和显示 Icon
(尽管在我看来,至少 SMS 消息没有图标)。IMAPIMessageID
和 IMAPIFolderID
扩展自 IEntryID
,其中包含消息和文件夹的特定 ID。然后可以使用 ID 来打开文件夹和消息。每个公开接口的实现都提供了更“人性化”的方式来获取和设置信息。例如,而不是进行数据库请求来获取邮件主题或时间,IMAPIMessage
接口具有 string Subject { get; }
和 DateTime LocalDeliveryTime { get; }
属性,它们会进行所有必要的转换。
Using the Code
此处提供的解决方案包含三个项目:最小化的 C++ MAPI 封装库(MAPIlib)、C# MAPI 封装库(MAPIdotnet)以及一个用于 PocketPC 和 SmartPhones 的示例项目,用于显示消息及其部分属性(PocketMail)。
如果在尝试访问原生 Win32 MAPIlib.dll 时出现异常,可能是因为它在部署解决方案时没有被复制到目录中。要执行此操作,请将“现有项”添加到 PocketMail
项目中,并导航到 MAPIlib/bin/[Release 或 Debug]/MAPIlib.dll。 **不要** 点击“打开”。点击“打开”按钮 **旁边** 的小下拉箭头,然后选择“另存为链接”。然后,在该文件的属性框中,选择“生成操作 = 内容”和“复制到输出目录 = 始终”。
警告:我目前正在对发生项目事件(例如,新邮件到达、邮件被删除、邮件主题被更改等)进行“优雅化”处理,因此建议忽略 cemapi.IMAPIAdviseSink
。
消息存储事件
IMAPIMsgStore
现在可以注册事件,例如新邮件到达、消息/文件夹更改/移动/复制/删除。IMAPIMsgStore
公开三个事件。除了订阅文件夹和消息的公开事件外,还必须通过设置 EventNotifyMask
来屏蔽不同的事件类型。一个小提示,您会很快意识到,注册所有可能的事件是一场噩梦,因为每次发生某事时都会引发大约六个事件!
关注点
性能
在我编写库的过程中,我一直担心几乎所有东西都用 C# 实现会意味着它运行起来会像狗一样。事实证明,我错了。在我的手机上,我的收件箱中有 1500 多封邮件,获取其中每一封(这需要为每一封创建至少两个类并获取它们的所有信息)只需要一秒多一点。相比之下,构建 PocketMail 程序提供的节点树(甚至还没有显示)大约需要十秒钟。然后,将填充的节点添加到 TreeView
(TreeView.Nodes.AddRange(TreeNode[] nodes)
)需要超过三十秒!
持续开发
MAPIdotnet 现在在 SourceForge 上有一个专门的项目,链接在此处。项目页面提供了对 SVN 存储库的访问,其中包含最新的添加和新项目。如果您也有兴趣,请随时与我联系,成为一名具有提交权限的开发者。
历史
- 2007 年 10 月 31 日 -
MAPIdotnet
的初始发布。 - 2008 年 2 月 13 日 - 更新,包括添加了
Message
和Folder
ID,删除消息,撰写新消息,以及消息/文件夹/存储事件。 - 2008 年 4 月 28 日 - 添加了关于 SourceForge 项目详细信息的说明。