CEMAPI实战攻略(三)——操作信箱中的短信息(下)
By 吴春雷
QQ:819543772
Email:wuchunlei@163.com
6. 解析原始短消息
当成功获取原始信息以后,还不能从中直接获得短信正文等我们想要的内容,要想得到这些内容,还需要对原始信息作一些操作。还记得我们前面提过的短消息的组成结构吗?下面的内容从原始短信中获取每个结构中的内容。
a) 获取正文
前面提到了Subject和body的关系,在发送短信的时候,Subject的内容后面加上一个\n,在加上body中的内容作为一条短信的正文。但奇怪的是,使用cemapi获取短信正文的时候不能使用PR_BODY属性,而需要获得PR_SUBJECT属性,这一点请读者注意。
然后就是老套路了,要想获得对象中某种属性的内容,首先需要建立一个动态结构体对象,这次我们只需要获取原始信息中PR_SUBJECT属性的内容,因此结构体的第一个ULONG参数应该为1,第二个参数ULONG*数组元素数应该也为1,值为PR_SUBJECT。然后利用IMessage::GetProps方法来获取PR_SUBJECT属性。非常简单,程序列表如下:
//取短信内容
HRESULT hr;
ULONG cValues = 0;
SPropValue *pspvSubject= NULL;
CString strSubject(_T(""));
SizedSPropTagArray(1, sptaSubject) = { 1, PR_SUBJECT};
LPMESSAGE m_pMsg=NULL;
m_pMsg=…… //取消息指针,方法见上文
ASSERT(m_pMsg!=NULL);
hr = m_pMsg->GetProps((SPropTagArray *) & sptaSubject, MAPI_UNICODE, &cValues, & pspvSubject); //从原始消息中提取属性
if(FAILED(hr))
{
//错误处理
}
if(pspvSubject ->ulPropTag!=10) //每次返回10的时候程序都会抛出异常,这里假设为错误代码
{
strSubject= pspvSubject ->Value.lpszW; //消息正文
}
else {strSubject=_T("");}
当pspvSubject中的ulPropTag的值等于10的时候,系统都会跑出一个不知道是什么类型的异常,我想了各种办法也没法捕获它。所以只好在获取消息以后判断一下ulPropTag属性。如果有朋友了解原因,希望能给我发个邮件告诉我。
b) 获取发送方电话号码
发送方电话号码对应标志PR_SENDER_EMAIL_ADDRESS。获取方式与获取短信正文相同,源代码如下:
//取发信者号码
HRESULT hr;
ULONG cValues = 0;
SPropValue *pspvFromTel = NULL;
LPMESSAGE m_pMsg=NULL;
CString strFromTel(_T(""));
m_pMsg=……; //取消息指针
ASSERT(m_pMsg);
SizedSPropTagArray(1, sptaFromTel) = { 1, PR_SENDER_EMAIL_ADDRESS};
hr = m_pMsg->GetProps((SPropTagArray *) &sptaFromTel, MAPI_UNICODE, &cValues, & pspvFromTel);
if(FAILED(hr))
{
//异常处理
}
if(203358239==pspvFromTel->ulPropTag){ //203358239为该项存储在的标志?
strFromTel= pspvFromTel ->Value.lpszW;
}
else strFromTel=_T("");
同样的尴尬,如果pspvEmail->ulPropTag的值不为203358239的时候,系统会抛出一个怎么也抓不到的异常,所以这里只能先对该成员的值进行判断。
c) 获取接收方电话号码
这里稍稍有些麻烦,但是也不用担心,因为使用到的方法和结构体只有一个前面没见到过的,那就是ADRLIST结构体,该结构体定义如下:
typedef struct _ADRLIST
{
ULONG cEntries;
ADRENTRY aEntries[MAPI_DIM];
} ADRLIST, FAR * LPADRLIST;
typedef struct _ADRENTRY
{
ULONG ulReserved1; /* Never used */
ULONG cValues;
LPSPropValue rgPropVals;
} ADRENTRY, FAR * LPADRENTRY;
MAPI_DIM的值为1。这个定义是不是有些眼熟?回忆一下结构体SRowSet的定义,是不是除了成员命名不一样以外,其它的东西都是相同的?那是不是预示着ADRLIST这个结构体与SRowSet有相近的功能呢?恭喜您,您的猜测是正确的。这个结构体也用于在IMAPITable::QueryRows方法中返回行记录,只不过在使用ADRLIST之前,无需我们再使用IMAPITable::SetColumns来为表格设置行记录的格式。ADRLIST所代表的内容是联系人列表的结构体。
短信的联系人不止一位,因此很容易想到联系人会是一个列表,在Cemapi中,想到了表自然就想到了IMAPITable接口对象,这里有一点小小的不同,我们将使用IMAPITable::GetRecipientTable方法来直接获取联系人列表,而不需要构建动态结构体变量,方法的定义如下:
HRESULT IMAPITable::GetRecipientTable(ULONG,IMAPITable**);
返回用来判断方法是否正确执行。参数列表:
ULONG:某种标志,mapidefs.h中并没有对这个标志的定义,短信应用中一般取0。
IMAPITable**:该参数用于返回联系人列表。
当成功获取联系人列表之后,我们就可以依次遍历每个联系的所有属性,如果存在PR_EMAIL_ADDRESS属性则这个联系人就是我们要获取的。代码如下:
IMAPITable* m_pTable = NULL;
CString strToTel(_T(""));
ADRLIST * pstToRows=NULL;
HRESULT hr;
LPMESSAGE m_pMsg=NULL;
m_pMsg=……; //取消息指针
//获取联系人信息列表
m_pMsg->GetRecipientTable( NULL, &m_pTable );
if(NULL==m_pTable)
{
//没有接收人信息,可能是InBox中的短信,直接退出
}
//获取每个联系人信息
while(!FAILED(hr = pTable->QueryRows(1, 0, (LPSRowSet*)& pstToRows)))
{
if(pstToRows ->cEntries == 0 ) //没有联系人
break;
for(int n = 0; n < pstToRows ->cEntries; n++ )
{
//遍历每个联系人属性
for(int i = 0; i < pstToRows ->aEntries[n].cValues ; i++)
{
//判断如果是PR_EMAIL_ADDRESS属性,那么就找到了联系人地址
if( PR_EMAIL_ADDRESS == pstToRows ->aEntries[n].rgPropVals[i].ulPropTag )
{
//联系人地址
}
}
}
MAPIFreeBuffer(pstToRows);
}
d) 获取发送/接收/创建时间
根据短信类型的不同,使用PR_MESSAGE_DELIVERY_TIME属性可以获取短信的发送/接收/创建时间。还是老方法,创建动态数组SPropTagArray,利用GetProps方法获取消息的PR_MESSAGE_DELIVERY_TIME属性所对应的值。然后从_PV联合体的ft成员获取UTC文件时间,然后采用系统API FileTimeToLocalFileTime函数将UTC时间转换为本地文件时间,最后用FileTmieToSystemTime函数将本地文件时间转换为系统时间。源代码如下:
HRESULT hr;
ULONG cValues = 0;
SPropValue *pspvTime = NULL;
LPMESSAGE m_pMsg=NULL;
m_pMsg=……; //取消息指针
SizedSPropTagArray(1, sptaTime) = { 1, PR_MESSAGE_DELIVERY_TIME }; //建立动态结构体
hr =m_pMsg->GetProps((SPropTagArray *) &sptaTime, MAPI_UNICODE, &cValues, & pspvTime);
if(FAILED(hr))
{
//错误处理
}
if(pspvTime ->ulPropTag!=10){ //等于时通常会抛出异常,因此猜测等于的时候读取失败
//格式化时间
FILETIME ft;
SYSTEMTIME stTime;
FileTimeToLocalFileTime(&pspvTime ->Value.ft,&ft);
FileTimeToSystemTime(&ft,&stTime);
}
7. 获取短消息ID
短消息ID(ENTRYID)是唯一标志一条短消息的标识,通过它我们可以找到这条消息在具体信箱中的位置,进而对这条消息进行删除、移动等操作。获取ENTRYID的方式有两种,一种是前面讲到过的遍历Folder下IMAPITable对象的行记录时获得的。例如:
m_pRows->aRow[0].lpProps[0].Value.bin.cb
m_pRows->aRow[0].lpProps[0].Value.bin.lpb
另一种方式是使用GetProps方法直接获取消息中PR_ENTRYID属性所对应的值,方法如下:
//获取EntryID
ULONG cValues = 0;
SPropValue *pspvID= NULL;
SizedSPropTagArray(1, sptaID ) = { 1, PR_ENTRYID};
hr = m_pMsg->GetProps((SPropTagArray *) &sptaID, MAPI_UNICODE, &cValues, &pspvID);
pspvID->Value.bin就是需要的ENTRYID
8. 在具体信箱中建立一条短消息
在建立一条消息前,很自然的会想到,我需要在哪个具体信箱中建立该消息,因此建立消息的第一步就是获取指向具体信箱(Folder)的对象指针,获取IMAPIFolder指针的方法见前面的内容。假设我们已经获取了Folder的指针对象m_pFolder,下面的步骤能够让我们在该信箱中建立一条新消息。
a) 建立一条空消息
使用IMAPIFolder::CreateMessage方法可以在具体信箱中建立一条空消息。该方法定义如下:
HRESULT IMAPIFolder::CreateMessage(LPCIID,ULONG,IMessage **)
返回值用来标识方法是否成功执行。参数列表:
LPCIID:GUID结构的指针,直接设置为NULL即可。
ULONG:某种未知的标志,希望大家补充,直接设置为0可以正常工作。
IMessage **:用于返回刚刚建立的消息对象指针。
b) 添加联系人
上一步建立的消息中没有任何内容,现在来为其加入接收人信息。首先要做的是建立接收人对应的结构体ADRLIST中ADRENETRY中的SPropValue属性,该属性用于指定接收人类型,对方地址的类型(还有可能是邮件哦,别忘了CEMAPI不止是用来读取短信的),接收人地址等属性,我们主要设置这三个属性。代码如下:
SPropValue propRecipient[3];
ZeroMemory(&propRecipient, sizeof(propRecipient));
propRecipient[0].ulPropTag = PR_RECIPIENT_TYPE; //接收人类型
propRecipient[0].Value.l = MAPI_TO;
propRecipient[1].ulPropTag = PR_ADDRTYPE; //地址类型
propRecipient[1].Value.lpszW = _T("SMS"); //短信
propRecipient[2].ulPropTag = PR_EMAIL_ADDRESS; //地址
propRecipient[2].Value.lpszW = (LPWSTR)CString(_T(“15011056698”)); //设置接收者号码
然后,将属性添加到联系人结构体ADDLIST对象中,并使用IMessage::ModifyRecipient方法将该对象设置到消息中去。该方法定义为:
HRESULT IMessage::ModifyRecipient(ULONG,ADDLIST *);
返回值标志方法是否正确运行,参数列表:
ULONG:操作类型,MODRECIP_ADD为添加联系人信息,MODRECIP_MODIFY为修改联系人信息,MODRECIP_REMOVE为删除联系人信息。标志定义如下:
#define MODRECIP_ADD ((ULONG) 0x00000002)
#define MODRECIP_MODIFY ((ULONG) 0x00000004)
#define MODRECIP_REMOVE ((ULONG) 0x00000008)
ADDLIST *:用于将要修改的联系人信息传递给方法。
设置联系人的代码如下:
ADRLIST adrlist;
adrlist.cEntries = 1;
adrlist.aEntries[0].cValues = 3; \\表示设置了三个SPropValue属性
adrlist.aEntries[0].rgPropVals = (LPSPropValue)(&propRecipient);
hr = m_pMsg->ModifyRecipients(MODRECIP_ADD, &adrlist);
if (FAILED(hr))
{
//异常处理
}
c) 添加正文,发送方号码和创建时间等属性
为了能够使短信能够支持中文,在为短信对象添加正文时要指定正文的编码方式,用以下代码可以获取短信的UNICODE属性
MAPINAMEID idName;
ZeroMemory(&idName, sizeof(MAPINAMEID));
GUID PS_MAPI={0x00020328,0,0,0xC0,0,0,0,0,0,0,0x46};
idName.lpguid = (LPGUID)&PS_MAPI;
idName.ulKind = MNID_STRING;
idName.Kind.lpwstrName = L"SMS:Unicode";
LPMAPINAMEID pidName = &idName;
LPSPropTagArray pPropTag = NULL;
hr = m_pMsg->GetIDsFromNames(1, &pidName, MAPI_CREATE, &pPropTag);
这段代码是我从微软的网站上拷贝下来的,国内也有很多人在用,但却没有过多的解释。由于参考资料有限,我只能根据猜测来尝试着解释一下这段代码,如果某位达人发现我的解释有问题,希望赶紧与我联系,以便尽快修改,不要误导读者。
这里有一个命名属性集(Named properties )或叫属性识别码的概念,它是一组由GUID和名字(name)组成的属性集合,注册在MAPI系统中,用来唯一标志系统中使用到的某一个属性及其相关的详细信息,比如UNICODE属性。GUID和名字组合成为命名属性集的NAME,而命名属性集还有一个ID用为唯一标明该属性。当我们得到命名属性集的NAME或ID以后,我们可以通过GetIDsFromNames和GetNamesFromIDs获取未知的那个对象。方法IMessage::GetIDsFromNames的原型定义为:
HRESULT IMessage::GetIDsFromNames(LPCIID,ULONG,SPropTagArrray **);
方法返回值标志了方法是否正确执行。参数列表:
LPCIID:GUID结构体,为什么这里要设置成1,不详。
ULONG:某种标志。可以取MAPI_CREATE或STREAM_APPEND,意义不详,这里取MAPI_CREATE
SPropTagArray**:用于返回GUID和name对应的命名属性集中的属性列表
获得了命名属性集中的属性列表以后,我们就可以开始设置短信的发送号码,正文以及创建时间了。
设置方法没有离开我们前面所讨论的内容,这里直接给出代码,然后在对代码做简要的解释。
//设置短信属性
SPropValue props[8];
ZeroMemory(&props, sizeof(props));
props[0].ulPropTag = PR_MESSAGE_CLASS; // (1)设置显示窗体类型为“短信”
props[0].Value.lpszW = TEXT("IPM.SMStext");
props[1].ulPropTag = PR_SUBJECT; // (2)设置正文
props[1].Value.lpszW = (LPWSTR)strBody.GetBuffer();
props[2].ulPropTag = PR_SENDER_EMAIL_ADDRESS;
props[2].Value.lpszW = (LPWSTR)strFrom.GetBuffer(); // (3)设置发送号码
props[3].ulPropTag = PR_MSG_STATUS; //(4)标志设置信息类型
props[3].Value.ul = MSGSTATUS_RECTYPE_SMS; //(5)设置具体类型
props[4].ulPropTag = PR_MESSAGE_FLAGS; //(6)标志设置发送属性
props[4].Value.ul = MSGFLAG_FROMME | MSGFLAG_UNSENT; //(7)设置具体发送属性
props[5].ulPropTag = CHANGE_PROP_TYPE(pPropTag[0].aulPropTag[0], PT_BOOLEAN); //设置UNICODE属性,前面用GetIDsFromNames方法获取的
props[5].Value.b = TRUE;
//设置日期
SYSTEMTIME st;
FILETIME ft;
GetSystem Time(&st);
SystemTimeToFileTime(&st,&ft);
props[6].ulPropTag = PR_MESSAGE_DELIVERY_TIME; //(8)设置建立时间
props[6].Value.ft=ft;
props[6].Value.b = TRUE;
hr = m_pMsg->SetProps(sizeof(props) / sizeof(props[0]), (LPSPropValue)&props, NULL);//(9)设置属性
if (FAILED(hr))
{
//异常处理
}
我在代码中关键的位置打了标志,下面我们一条一条去看。
(1) 注意 PR_MESSAGE_CLASS标志告诉系统我要创建的是短信,由于后面要设置PR_SUBJECT属性,如果不设置这个标志,那么我们应该写到短信正文中的内容就会被写进主题中。
(2) 注意设置短信的正文要用PR_SUBJECT属性,而不是PR_BODY属性
(3) 设置发送号码,它将标志该条短信来自那里。
(4) PR_MSG_STATUS标示告诉系统,当前这个SPropValue将被用于设置信息类型。
(5) MSGSTATUS_RECTYPE_SMS标识告诉系统,现在是为短信填写属性,这里还有一个可选项MSGSTATUS_RECTYPE_SMTP,用于标志邮件。本文中不用。
(6) PR_MESSAGE_FLAGS标识告诉系统,当前这个SPropValue将被用于设置信息发送属性
(7) MSGFLAG_FROMME | MSGFLAG_UNSENT :分别标志这个短信来自本地并且从未发送。与之相关的标识还有:#define MSGFLAG_READ ((ULONG) 0x00000001) //已读
#define MSGFLAG_UNMODIFIED ((ULONG) 0x00000002) //未读
#define MSGFLAG_SUBMIT ((ULONG) 0x00000004) //已提交
#define MSGFLAG_UNSENT ((ULONG) 0x00000008) //未发送
#define MSGFLAG_HASATTACH ((ULONG) 0x00000010) //有附件
#define MSGFLAG_FROMME ((ULONG) 0x00000020) //来自本地
#define MSGFLAG_ASSOCIATED ((ULONG) 0x00000040) //不明
#define MSGFLAG_RESEND ((ULONG) 0x00000080) //重发
#define MSGFLAG_RN_PENDING ((ULONG) 0x00000100) //不明
#define MSGFLAG_NRN_PENDING ((ULONG) 0x00000200) //不明
(8) 设置建立时间,这里注意要把系统时间转换为UFC文件时间。
(9) SetProps方法用于给消息设置属性,其原型为
HRESULT SetProps(ULONG cValues, SPropValue *lpPropArray, SPropProblemArray FAR ** lppProblems)
返回值表示方法是否执行成功。参数列表:
cValues:表示lpPropArray数组中元素的个数。
lpPropArray:要设置的属性列表
lppProblems:字面上理解是为了返回某些异常?没有找到相关资料,大家来补充吧。
d) 最后别忘了SaveChange
做完上述操作以后,短信还不能显示在具体邮箱中,最后需要调用一下IMessage::SaveChange方法确认修改,当该方法成功调用后,新的短信将会显示在具体信箱中。
e) 为什么我发送的短消息内容写在了主题上而正文却是空的
注意(c)中的第(1)条:“PR_MESSAGE_CLASS标志告诉系统我要创建的是短信,由于后面要设置PR_SUBJECT属性,如果不设置这个标志,那么我们应该写到短信正文中的内容就会被写进主题中。”单独列出个标题来强调一下,否则使用WM2003以前版本的朋友会很困惑。
9. 从具体信箱中删除一条短消息
删除一条短信就显得容易多了,还记得我们获取的ENTRYID吗?利用这个ID设置一个名为ENTRYLIST的结构体,然后直接调用IMAPIFolder::DeleteMessages方法就可以了。ENTRYLIST结构体定义:
typedef struct _SBinaryArray
{
ULONG cValues;
SBinary FAR *lpbin;
} SBinaryArray;
typedef SBinaryArray ENTRYLIST, FAR *LPENTRYLIST;
很明显,这是一个ENTRYLIST就是SBinary的列表,而SBinary用于保存短信的ENTRYID。ENTRYLIST中可以设置一组SBinary对象,就意味着DeleteMessages可以直接删除一组短消息(当然也可以删除一条)。这里只给出DeleteMessages的调用方式,参数的含义就不解释了(因为我找不到资料L)。删除短信的源代码如下:
void CMsgControl::DeleteMsg(SBinary bin)
{
HRESULT hr=0;
//删除短消息
ENTRYLIST stEntryList;
stEntryList.cValues=1; //删除1条短信
stEntryList.lpbin=new SBinary[1]; //这里注意要分配空间
stEntryList.lpbin[0].cb=bin.cb;
stEntryList.lpbin[0].lpb=bin.lpb;
hr=m_pFolder->DeleteMessages(&stEntryList,0,NULL,0);
delete [] stEntryList.lpbin; //释放内存控件
stEntryList.lpbin=NULL;
}
10. 将短消息移动到某个具体的信箱
有了删除短信和创建短信的方法以后,聪明的您该能够想到如何移动短信了吧?在源位置获取短信——在目的位置创建新短信——删除原位置的短信。哈哈,就这么简单,只要注意事务性就OK了,其它的没有什么可说的。
11. 将InBox中的信息标记为已读或未读
前面已经提到过如何设置短信的状态了,设置短信状态只需要将PR_MESSAGE_FLAGS属性设置为MSGFLAG_READ(已读)或MSGFLAG_UNMODIFIED(未读)就可以了。设置方法前面已经说过,不再赘述。源代码如下:
//标记已读和未读标志
void CMsgInBox::Mark(IMessage *m_pMsg,int nType)
{
ASSERT(m_pMsg!=NULL);
SPropValue props[1];
if(0==nType){ //标记为已读
props[0].ulPropTag = PR_MESSAGE_FLAGS;
props[0].Value.ul = MSGFLAG_READ;
m_pMsg->SetProps(sizeof(props) / sizeof(props[0]), (LPSPropValue)&props, NULL);
m_pMsg->SaveChanges(0); //保存改变
}
else if(1==nType) //标记为未读
{
props[0].ulPropTag = PR_MESSAGE_FLAGS;
props[0].Value.ul = MSGFLAG_UNMODIFIED;
m_pMsg->SetProps(sizeof(props) / sizeof(props[0]), (LPSPropValue)&props, NULL);
m_pMsg->SaveChanges(0); //保存改变
}
else
{
throw(CMsgException(_T("未知的标记值"),_T("CMsgInBox->Mark"),ERR_UNKNOW_FLAG));
}
}
12. 本节所涉及的源程序
比较多,暂时略。我会整理好提供下载的。