CEMAPI实战攻略
by 吴春雷
QQ:819543772
Email:wuchunlei@163.com
二.建立与短信信箱的连接
上一部分已经讨论过,如何搭建开发和测试环境,以及如何初始化CEMAPI,再继续这一部分的讨论之前,我们先要澄清几个概念。第一个是会话(Seesion),相信开发网络应用的朋友都不陌生,为了提高通讯效率降低通讯开销,有时候我们需要再目标与本地之间创建一个通道,在通道创建之初,目标与本地先做一些列的响应和请求确认两边的身份,当通道建立以后,目标与本地之间的通讯过程中就不再涉及两边的身份确认,这通常目标与本地之间的建立的通道,通常被称作会话,也就是Session。在使用Cemapi读取短信之前,应用程序也需要与设备上的信息(邮件)系统之间建立一个Session,用以 确认双方的身份,这是采用Cemapi读取短信的第一步。第二个概念是短消息(邮件)仓库(MsgStore),在WM中,邮件和短消息是属于一个系统的,Session建立了与这个系统之间的连接,然后必须告诉系统,我们的程序是要对邮件功能进行操作,还是要对短信功能进行操作,通过调用相应的函数(后面会介绍),MsgStore会指向我们需要操作的短信或邮件的仓库上。第三个概念是信箱,或者叫文件夹(Folder),当获得了指向一个具体仓库的MsgStore以后,下一步就需要获取具体的信箱(文件夹)了,比如当程序确定了希望对收件箱还是发件箱进行操作以后,Folder将会指向我们想要操作的具体的信箱。
OK,澄清了这三个概念,就可以进一步讨论,如何建立会话,获取具体信箱了。
1. 会话接口IMAPISession
从mapidefs.h中我们可以看到,通过DECLARE_MAPI_INTERFACE_这个宏使IMAPISession派生自IUnKnow接口,IUnKnow接口中定义引用技术等与COM有关的基本操作,关于IUnKnow的详细内容大家可以参见COM技术的相关资料。IMAPISession接口中值得注意的一个函数是GetMsgStoresTable,后面我们将通过调用该函数获取短信(邮件)仓库的列表。
2. 如何创建与MAPI的会话
Cemapi中,我们将使用MAPILogonEx函数建立与短信(邮件)系统的会话,MAPILogonEx在Mapix.h中的定义如下:
typedef HRESULT (STDMETHODCALLTYPE MAPILOGONEX)(
ULONG ulUIParam,
LPTSTR lpszProfileName,
LPTSTR lpszPassword,
ULONG ulFlags,
LPMAPISESSION FAR * lppSession
);
MAPILOGONEX MAPILogonEx;
从定义中可以看出MAPILogonEx函数返回一个HRESULT类型,采用宏FAILED和SUCCESSED可以判断函数是否成功返回。同时,该函数有五个参数,这五个参数分别表示,短信(邮件)系统登陆UI的现实方式以及Session的共享方式,配置文件的文件名,邮箱密码,编码方式(默认)和指向IMAPISession接口指针的指针,对于短信应用程序的开发,前四个参数均无意义,可以直接设置为NULL。如果函数调用成功,我们将会从最后一个参数那里得到短信(邮件)系统的Session指针。调用方法如下:
IMAPISession *m_pSession=NULL;
hr=MAPILogonEx(NULL,NULL,NULL,NULL,&m_pSession);
if(FAILED(hr) || NULL==m_pSession)
{
//异常处理
}
3. 如何终止与短信(邮件)系统的会话,并释放Session对象
使用IMAPISession接口中Logoff方法可以终止与短信(邮件)系统的会话,Logoff方法定义为:
HRESULT IMAPISession::Logoff(ULONG ulUIParam,ULONG ulFlags,ULONG ulReserved);
方法返回一个HRESULT对象,通过它可以判断调用是否成功。前两个参数的意义与MAPILogonEx中的同名参数相同,最后一个参数保留不用。对于短信操作来说,三个参数均可设置为NULL。
当成功Logoff以后,如果确信不再需要Session对象以后,可以通过Release方法释放对象。源代码如下:
if(NULL!=m_pSession) //释放Session
{
HRESULT hr=m_pSession->Logoff(NULL,NULL,NULL);
if(FAILED(hr))
{
//异常处理
}
m_pSession->Release();
m_pSession=NULL;
}
4. 短信(邮件)仓库接口IMsgStore
IMsgStore继承自IMAPIProp接口,而IMAPIProp接口又继承自IUnknow接口,这个接口中值得我们重点关注的函数有GetProps,OpenEntry两个函数,后面我们将会通过这两个函数获取具体信箱(Folder)对象。
5. 建立与短信仓库的连接
在实现连接以前,先来看一个很有意思的宏
#define SizedSPropTagArray(_ctag, _name) \
struct _SPropTagArray_ ## _name \
{ \
ULONG cValues; \
ULONG aulPropTag[_ctag]; \
} _name
为什么说这个结构体有意思,仔细看一下就知道了,利用了一个带参数的宏,实现了动态声明结构体的功能,更奇妙的是,连结构体的名称都可以动态创建。大家别怪我顾洛寡闻,这种声明方式还真是不太常见。这个数据结构在Cemapi中扮演一个很重要的角色,通过定制的实现它,可以告诉函数,我希望获取或设置那些属性。看下面一段程序:
SizedSPropTagArray(2 , Columns) = {
2,
PR_ENTRYID, //Entry ID
PR_DISPLAY_NAME //Display Name
};
这段程序动态的声明了一个名为_SPropTagArray_Columns的结构体,并且声明了一个名为Columns的结构体变量。这个结构体中有一个ULONG类型的成员变量,和一个长度为2的ULONG类型的数组成员组成。结构体中cValues的值被初始化为2,aulPropTag[0]=PR_ENTRYID,aulProTag[1]=PR_DISPLAY_NAME。
这里面涉及到了两个常量符号,PR_ENTRYID和PR_DISPLAY_NAME,这两个符号分别表示对象ID和显示名称,这里所说的对象可以是短信(邮件)存储仓库,也可是具体信箱Folder,还可以是短消息本身,调用不同的函数,这些符号会被解释为具体对象的某些属性。
另外还有一个重要的接口需要说明,那就是IMAPITable,这个接口也从IUnKnow中继承。在WM系统中的短信(邮件)仓库、具体信箱Folder以及Folder中的短信都不是唯一的,在使用Cemapi中的接口方法获取这些对象的时候,将会采用表的形式返回结果,IMAPITable接口的作用就是用于描述这个表的结构。
有了这两个类型作为基础,我们就可以通过尝试获取WM系统中的短信(邮件)仓库列表了,前面提到了IMAPISession接口一个方法GetMsgStoresTable,从名字上应该就很直观的知道了这个方法的功能,即获取MsgStore(短信邮件仓库)列表。该方法定义为:
HRESULT IMAPISession::GetMsgStoresTable(ULONG ulFlags,LPMAPITABLE FAR * lppTable)
返回值依旧标志方法是否运行成功,不再赘述。参数中
ulFlags:表示字符编码类型,这里好像只有MAPI_UNICODE标志供选择。
lppTable:实际是一个IMAPITable **类型,该方法通过它返回MsgStore(短信邮件仓库)列表。
当GetMsgStoresTable方法成功获取了IMAPITable接口的对象以后,这时该对象里面的数据还是以原始的方式组织的,我们无法获取表中的记录,这时候就需要调用IMAPITable接口中的SetColumns方法来告诉IMAPTABLE对象,内部数据将以什么形式进行组织。该方法定义为:
HRESULT IMAPITable::SetColumns(LPSPropTagArray , ULONG);
返回值用于判断方法调用是否成功。参数说明:
LPSPropTagArray:用于说明IMAPITable中记录的组织形式,把前面提到过的Columns对象作为参数传入,则表示告诉IMAPITable对象,表格中每条记录有两列,第一列是对象ID(PR_ENTRYID),第二列是对象现实名称(PR_DISPLAY_NAME)。
ULONG:某种标志,一般设置为0,这里我没有找到相关资料,希望高手们补充。
有了表格,有了记录的结构,下一步要做什么应该很容易就能想到。Yes ,取表格中的所有记录,并且遍历这些记录,查找显示名称(PR_DISPLAY_NAME)为SMS的记录。这里再介绍一种数据结构SRowSet,其定义如下:
typedef struct _SRowSet
{
ULONG cRows; /* 行数 */
SRow aRow[MAPI_DIM]; /* 行记录具体信息 */
} SRowSet, FAR * LPSRowSet;
很有意思,MAPI_DIM的值为1,但是绝不是说所有从IMAPITable中取出的行记录都只有一列,恰恰相反,列的数量是由我们前面提到的动态结构体变量Columns中的cValues的值来决定的,这里请读者朋友们注意。SRow也为一个结构体,其定义如下:
typedef struct _SRow
{
ULONG ulAdrEntryPad;
ULONG cValues; /* 用于标志lpProps成员的数量 */
LPSPropValue lpProps; /* 属性结构体*/
} SRow, FAR * LPSRow;
lpProps成员所对应的结构体SPropValue才是行记录中真正的数据。其定义如下
typedef struct _SPropValue
{
ULONG ulPropTag; /*属性标志,常用于辅助判断属性值是否成功获取*/
ULONG dwAlignPad;
union _PV Value;
} SPropValue, FAR * LPSPropValue;
这个结构中Value成员非常有用,它由很多成员组成,每个成员对应着对象的一个属性,该联合体定义如下:
typedef union _PV
{
short int i; /* case PT_I2 */
LONG l; /* case PT_LONG */
ULONG ul; /* alias for PT_LONG */
float flt; /* case PT_R4 */
double dbl; /* case PT_DOUBLE */
unsigned short int b; /* case PT_BOOLEAN */
CURRENCY cur; /* case PT_CURRENCY */
double at; /* case PT_APPTIME */
FILETIME ft; /* case PT_SYSTIME */
LPSTR lpszA; /* case PT_STRING8 */
SBinary bin; /* case PT_BINARY */
LPWSTR lpszW; /* case PT_UNICODE */
LPGUID lpguid; /* case PT_CLSID */
LARGE_INTEGER li; /* case PT_I8 */
SShortArray MVi; /* case PT_MV_I2 */
SLongArray MVl; /* case PT_MV_LONG */
SRealArray MVflt; /* case PT_MV_R4 */
SDoubleArray MVdbl; /* case PT_MV_DOUBLE */
SCurrencyArray MVcur; /* case PT_MV_CURRENCY */
SAppTimeArray MVat; /* case PT_MV_APPTIME */
SDateTimeArray MVft; /* case PT_MV_SYSTIME */
SBinaryArray MVbin; /* case PT_MV_BINARY */
SLPSTRArray MVszA; /* case PT_MV_STRING8 */
SWStringArray MVszW; /* case PT_MV_UNICODE */
SGuidArray MVguid; /* case PT_MV_CLSID */
SLargeIntegerArray MVli; /* case PT_MV_I8 */
SCODE err; /* case PT_ERROR */
LONG x; /* case PT_NULL, PT_OBJECT (no usable value) */
} __UPV;
看到这么成员是不是眼有些花呀?我认为这些成员不必全部了解,因为我们不必像想孔乙己那样,知道茴香豆的茴字怎么写,还要知道有几种写法(当然您也可以不这么认为)。其实我们只需要知道ft,lpszA,lpszW以及bin这四个成员就可以了,他们分别代表发送(接收)时间,显示名称或消息标题或正文或发送号码或接受号码等字符串(ASCII),显示名称或消息标题或正文或发送号码或接受号码等字符串(UNICODE)以及对象的EntryID(对象可以是短信邮件仓库,可以是具体信箱Folder也可以是某条短信)。在这一小节中,我们只用到了lpszW和bin。SBinary也为一个结构体对象,它用来唯一标示某一对象的ID,其定义如下:
typedef struct _SBinary
{
ULONG cb;
LPBYTE lpb;
} SBinary, FAR *LPSBinary;
这里面的两个成员含义不必深究,我们只需要知道,这两个成员所组成的结构体对象SBinary可以作为唯一标示对象的ID,(对象依旧可以是短信邮件仓库,可以是具体信箱Folder也可以是某条短信,在这一小节中它表示短信邮箱仓库对象的ID。
IMAPITable中提供了QueryRows方法来获取行记录,其定义如下:
HRESULT IMAPITable::QueryRows(LONG,ULONG,SRowSet **);
返回值用于判断方法调用是否成功,这里要注意,如果取不到任何行记录的时候也会返回失败,因此可以用于判断行记录是否已经遍历完毕。参数说明:
LONG:希望获取多少行记录。
ULONG:标志可以是如下定义的符号之一,很抱歉,具体每种标志代表什么含义,并没有资料特别的说明,有兴趣的朋友可以研究一下。再短信应用中,这个值一般会设置为0。
#define TBL_LEAF_ROW ((ULONG) 1)
#define TBL_EMPTY_CATEGORY ((ULONG) 2)
#define TBL_EXPANDED_CATEGORY ((ULONG) 3)
#define TBL_COLLAPSED_CATEGORY ((ULONG) 4)
SRowSet **:这个参数用于返回查找到的行记录。
每次QueryRows成功执行以后,IMAPITable中的游标会自动移动第一个参数LONG行记录,直到遍历完毕为止。
现在我们已经获取短信邮件系统中的所有短信邮件仓库了,下面要做的就是找到显示名称为SMS的那个MsgStore仓库,并获去指向该仓库的对象指针。还记得Columns这个动态结构体变量吗?我们通过SetColumns方法给行记录定义了两列,第一列为对象ID(PR_ENTRYID),第二列为显示名称(PR_DISPLAY_NAME),那么每一个SRowSet对象中就会有两个SPropValue结构体对象,第一个就代表PR_ENTRYID,第二个则代表PR_DISPLAY_NAME,第一个SPropValue中的Value联合体中的bin成员有效,而第二个SPropValue中的Value联合体中的lpszW成员有效。如果我们使用QueryRows方法获取到的SRowSet *对象为m_pRows,则下面代码则表示上述说明内容。
m_pRows->aRow[0].lpProps[0].Value.bin为PR_ENTRYID
m_pRows->aRow[0].lpProps[1].Value.lpszW为PR_DISPLAY_NAME
有了对象ID,我们就可以通过IMAPISession中的OpenEntry方法获取短信仓库对象IMsgStore了。OpenEntry方法定义为:
HRESULT IMAPISession::OpenEntry(ULONG,LPENTRYID,LPCIID,ULONG,ULONG*,LPUNKNOW*);
返回值说明了方法调用是否成功,参数说明如下:
ULONG:短信邮件仓库的EntryId,也即对应的SBinary结构中的cb成员
LPENTRYID:短信邮件仓库的EntryId指针,也即对应的SBinary结构中的lpb成员
LPCIID:本质是一个指向GUID结构体变量的指针,若想更深入的了解GUID结构体请参考COM相关资料,这里只给出定义:
typedef struct _GUID { // size is 16
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;
ULONG:访问标志,cemapi中只支持最优访问方式,MAPI_BEST_ACCESS
ULONG*:用于返回Message类型
LPUNKNOW *:一个指向IUnKnow或其派生类指针的指针,用于返回派生自IUnknow接口的对象,这里是IMsgStore对象。
OK,相关的内容基本上已经介绍完了,说了很多,估计您已经看的云里雾里了,还是用一段完整程序来给上面的内容做一个总结吧。
IMAPITable *m_pTable = NULL;
HRESULT hr = 0;
SRowSet *m_pRows = NULL;
SizedSPropTagArray(2 , Columns) =
{
2 ,
PR_ENTRYID, //
PR_DISPLAY_NAME //Display Name
};
if(NULL==m_pSession)
{
//异常处理
}
hr=m_pSession->GetMsgStoresTable(MAPI_UNICODE , &m_pTable); //获取IMAPITable对象
if(FAILED(hr) || NULL==m_pTable)
{
//没有取到表结构或取表结构时出错
}
hr=m_pTable->SetColumns((LPSPropTagArray)&Columns, 0); //设置行记录结构
if(FAILED(hr))
{
//异常处理
}
while(SUCCEEDED(m_pTable->QueryRows(1, 0, &m_pRows))) //循环遍历所有行记录
{
if (NULL == m_pRows || m_pRows->cRows != 1)
{
break;
}
//查找显示名字为SMS的行记录
if (_tcsicmp(m_pRows->aRow[0].lpProps[1].Value.lpszW, _T("SMS")) == 0)
{
ULONG ulMsgType;
//则获取指向短信仓库的对象
hr=m_pSession->OpenEntry(m_pRows->aRow[0].lpProps[0].Value.bin.cb,
(LPENTRYID)m_pRows->aRow[0].lpProps[0].Value.bin.lpb,
NULL,
MAPI_BEST_ACCESS,
&ulMsgType,
(LPUNKNOWN*)&m_pMsgStore);
if(FAILED(hr) || NULL==m_pMsgStore)
{
//异常处理
}
break;
}
FreeProws(m_pRows); //释放
m_pRows = NULL;
}
if(m_pRows) //释放资源
{
FreeProws(m_pRows);
m_pRows = NULL;
}
6. 释放IMsgStore对象
IMsgStore接口提供了Release方法释放对象资源,调用方式如下:
if(NULL!=m_pMsgStore)
{
m_pMsgStore->Release();
}
7. 与某一具体信箱建立连接,获取具体信箱接口IMAPIFolder对象
获取具体信箱IMAPIFolder对象要比获取IMsgStore对象容易很多,因为在短信仓库MsgStore下,只有收件箱,发件箱,草稿箱,废件箱,已发送邮件箱5种具体信箱(Folder),我们可以通过指定要获取的信箱(Folder)类型来直接获取指向该具体信箱的IMAPIFolder对象。
首先,依旧需要建立一个动态的SPropTagArray结构体变量,用于告诉短信仓库IMsgStore对象,我们需要获取哪一个具体信箱的IMAPIFolder对象,代码如下:
SizedSPropTagArray(1, Columns) =
{
1,
PR_CE_IPM_INBOX_ENTRYID /*表示要获取指向系统收件箱的IMAPIFolder对象*/
};
用于表示具体信箱(Folder)的标志:
PR_CE_IPM_INBOX_ENTRYID:系统收件箱
PR_CE_IPM_OUTBOX_ENTRYID:系统发件箱
PR_CE_IPM_DRAFTS_ENTRYID:草稿箱
PR_IPM_SENTMAIL_ENTRYID:已发邮件箱
PR_IPM_WASTEBASKET_ENTRYID):废件箱
然后我们需要用一个新的方法GetProps来获取具体信箱(Folder)的属性信息,其实我们的主要目的是获取属性中该具体信箱的EntryID。该方法被定义为:
HRESULT IMsgStore::GetProps(SPropTagArray *,ULONG,ULONG *,SPropTagArray**);
方法返回值标志方法是否执行成功。参数说明:
SPropTagArray * :利用前面动态结构体对象Columns,告诉IMsgStore对象,我需要取哪个具体信箱的属性。
ULONG:指明当前的编码方式,MAPI_UNICODE
SPropTagArray**:用于返回从具体信箱中获取的属性
最后用IMsgStore对象的OpenEntry方法建立获取指向具体信箱的IMAPIFolder接口对象。该方法的定义与IMAPISession中的同名对象相同,这里不再赘述。
获取指向具体信箱的IMAPIFolder接口对象的源程序如下:
HRESULT hr=0;
LPSPropValue stProps = NULL;
ULONG ulValues = 0;
SizedSPropTagArray(1, Columns) =
{
1,
PR_CE_IPM_INBOX_ENTRYID /*表示要获取指向系统收件箱的IMAPIFolder对象*/
};
// 获取Folder的Entry ID,然后通过OpenEntry获得对象
m_pMsgStore->GetProps((LPSPropTagArray) &Columns, MAPI_UNICODE, &ulValues, &stProps);
hr=m_pMsgStore->OpenEntry(stProps[0].Value.bin.cb, (LPENTRYID)stProps[0].Value.bin.lpb, NULL, MAPI_MODIFY, NULL, (LPUNKNOWN*)&m_pFolder );
if(FAILED(hr) || NULL==m_pFolder)
{
//异常处理
}
MAPIFreeBuffer(stProps); //释放掉对象
8. 释放掉Folder对象
If(NULL!=m_pFoder)
{
m_pFolder->Release();
}
9. 本节所涉及到的源程序
//获取IMAPISession会话对象
void Session()
{
IMAPISession *m_pSession=NULL;
hr=MAPILogonEx(NULL,NULL,NULL,NULL,&m_pSession);
if(FAILED(hr) || NULL==m_pSession)
{
//异常处理
}
}
//获取指向短信仓库的IMsgStroe接口对象
void MsgStore()
{
IMAPITable *m_pTable = NULL;
HRESULT hr = 0;
SRowSet *m_pRows = NULL;
SizedSPropTagArray(2 , Columns) =
{
2 ,
PR_ENTRYID, //
PR_DISPLAY_NAME //Display Name
};
if(NULL==m_pSession)
{
//异常处理
}
hr=m_pSession->GetMsgStoresTable(MAPI_UNICODE , &m_pTable); //获取IMAPITable对象
if(FAILED(hr) || NULL==m_pTable)
{
//没有取到表结构或取表结构时出错
}
hr=m_pTable->SetColumns((LPSPropTagArray)&Columns, 0); //设置行记录结构
if(FAILED(hr))
{
//异常处理
}
while(SUCCEEDED(m_pTable->QueryRows(1, 0, &m_pRows))) //循环遍历所有行记录
{
if (NULL == m_pRows || m_pRows->cRows != 1)
{
break;
}
//查找显示名字为SMS的行记录
if (_tcsicmp(m_pRows->aRow[0].lpProps[1].Value.lpszW, _T("SMS")) == 0)
{
ULONG ulMsgType;
//则获取指向短信仓库的对象
hr=m_pSession->OpenEntry(m_pRows->aRow[0].lpProps[0].Value.bin.cb,
(LPENTRYID)m_pRows->aRow[0].lpProps[0].Value.bin.lpb,
NULL,
MAPI_BEST_ACCESS,
&ulMsgType,
(LPUNKNOWN*)&m_pMsgStore);
if(FAILED(hr) || NULL==m_pMsgStore)
{
//异常处理
}
break;
}
FreeProws(m_pRows); //释放
m_pRows = NULL;
}
if(m_pRows) //释放资源
{
FreeProws(m_pRows);
m_pRows = NULL;
}
}
//获取指向具体信箱的IMAPIFolder接口对象
void Folder(ULONG ulType)
{
HRESULT hr=0;
LPSPropValue stProps = NULL;
ULONG ulValues = 0;
ULONG ulTags[] = { 1, ulType};
// 获取Folder的Entry ID,然后通过OpenEntry获得对象
m_pMsgStore->GetProps((LPSPropTagArray) ulTags, MAPI_UNICODE, &ulValues, &stProps);
hr=m_pMsgStore->OpenEntry(stProps[0].Value.bin.cb, (LPENTRYID)stProps[0].Value.bin.lpb, NULL, MAPI_MODIFY, NULL, (LPUNKNOWN*)&m_pFolder );
if(FAILED(hr) || NULL==m_pFolder)
{
throw(CMsgException(_T("获取FOLDER失败!"),_T("CMsgControl->Folder"),ERR_GET_FOLDER));
}
MAPIFreeBuffer(stProps); //释放掉对象
}
//释放掉IMAPISession、IMsgStore、IMAPIFolder对象
void UnInit()
{
if(NULL!=m_pSession) //释放Session
{
m_pSession->Logoff(NULL,NULL,NULL);
m_pSession->Release();
m_pSession=NULL;
}
if(NULL!=m_pMsgStore) //释放MsgStore
{
m_pMsgStore->Release();
m_pMsgStore=NULL;
}
if(NULL!=m_pFolder) //释放Folder
{
m_pFolder->Release();
m_pMsgStore=NULL;
}
}