GPS Programming Tips for Windows Mobile - Part 1
NETCF: Memory leak... now what??
Supporting Kiosk-Applications on Windows Mobile (Technically achievable vs. supported)
Wireless Programming on Windows Mobile: supported or not supported?
Establishing GPRS Connection on Windows CE and Windows Mobile: Sample Codes
Disable WebBrowser's Context-Menu in NETCF applications
MAPI on Windows Mobile 6: Programmatically retrieve mail BODY (sample code)
Microsoft released a HotFix for NETCF v3.5 on Windows Mobile 6.1.4 onwards, to address basic functionalities of WebBrowser control
The right approach to get a Contact’s last communication (IItem’s PIMPR_SMARTPROP)
Remote Desktop Mobile (RDP Client) disconnects after 10 minutes of inactivity
Support Boundaries for Windows Mobile Programming (Developing Drivers, for example... Or even WiFi Programming)
Miei post in italiano sul team-blog del Supporto Tecnico agli Sviluppatori
Summary: this post contains a sample code showing a way to use MAPI to log and count the mails of a Windows Mobile 6's Outlook Mobile's Inbox folder into a file (sender, recipients, subject, date-time, BODY).
Recently I've been involved in another case about MAPI: this time the developers needed to programmatically retrieve the BODY of the mails stored in the ActiveSync account (synchronized with the remote Exchange server) and some other default values such as sender, recipients, date-time. I erroneously imagined that such an apparently simple task already had a sample code available somewhere, however this is not true, probably because InTheHand did an extremely good job on wrapping MAPI for NETCF developers and because, let's admit it, MAPI are not so friendly to use. Even so, in any case for native developers (and for managed ones aiming to wrap few MAPI or to create a native DLL exposing a function, as in this case), to get started you can read Jay Ongg's posts "Getting Started with MAPI" and "Practical Use of MAPI" on the Windows Mobile Dev Team's blog.
Furthermore, I discovered that Windows Mobile 5.0 and 6 behave differently when it comes to mail bodies, and I understood why: basically MAPI Specifications allow to store the information in different ways, and different platforms achieved the same results by using different approaches. And besides of that, on Windows Mobile 5.0 HTML formatting was not natively supported, therefore you don't find PR_BODY_HTML even defined the in the MAPI definitions header files (so you need a compilation-condition #if you want the same code to work on both), while in contrast on Windows Mobile 6 the default formatting is HTML, and accordingly to the documentation (Message Content Properties) the body for incoming messages can be retrieved through PR_BODY_HTML_A.
In conclusion, to make a robust client application you basically need to loop through the possible properties until a match is found: MAPI can be really straightforward in this case, once you know how to do it...
I talked too much, as usual... let's see some code in action:
1- In my case I wanted a native DLL to be invoked by a NETCF client: here it is the prototype of the exported function
//Accesses the "ActiveSync" MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt) //Returns the numbers of copied messages INTHEINBOX_API ULONG SaveMessagesIntoFile(); //being: (this is done by VS2008 when applying the template to export symbols -- you can also use a .def file) #ifdef INTHEINBOX_EXPORTS #define INTHEINBOX_API __declspec(dllexport) #else #define INTHEINBOX_API __declspec(dllimport) #endif
and here it is how it can be called in a test VB.NET client, for example:
<DllImport("InTheInbox.dll")> _ Private Function SaveMessagesIntoFile() As Integer End Function Sub Main() 'this MsgBox allows to attach the native debugger before the native DLL is used MsgBox("Attach the debugger to the process and click Ok to go") Dim NumberOfMessagesInInbox As UInteger = SaveMessagesIntoFile() MsgBox("Saved " + NumberOfMessagesInInbox.ToString() + " messages.") End Sub
2- Above all the exported function has to get the MAPI Session's "Message Store Table" (IMAPISession::GetMsgStoresTable) to retrieve the Message Store we're interested on, which in this case is the one whose PR_DISPLAY_NAME is "ActiveSync" (if you don't set any other mail-account, Windows Mobile has 2 stores, one for Outlook mails ("ActiveSync") and one for sms ("SMS"): for example Jay Ongg here accessed the "SMS" store). Once the message store is individuated and opened (IMAPISession::OpenMsgStore), the code invokes an internal function that takes the store pointer as argument and saves and counts messages from the Inbox MAPI Folder:
////////////////////////////////////////////////////////////////////////////// //SaveMessagesIntoFile //Accesses the "ActiveSync" MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt) //Returns the numbers of copied messages INTHEINBOX_API ULONG SaveMessagesIntoFile() { HRESULT hr = E_FAIL; LPENTRYID pEntryId = NULL; ULONG cbEntryId = 0; LPMAPITABLE ptbl; ULONG ulObjType = 0; SRowSet *prowset = NULL; IMsgStore* pStore = NULL; //Initialize MAPI COM Server hr = MAPIInitialize(NULL); CHR(hr); //Logon to MAPI and get a session pointer hr = MAPILogonEx(0, NULL, NULL, 0, (LPMAPISESSION *)&m_pSession); CHR(hr); //variable to be used when setting the columns of the table we're going to retrieve static const SizedSPropTagArray(2, spta) = { 2, PR_DISPLAY_NAME, PR_ENTRYID }; // Get the table of accounts hr = m_pSession->GetMsgStoresTable(0, &ptbl); CHR(hr); // set the columns of the table we will query hr = ptbl->SetColumns((SPropTagArray *) &spta, 0); CHR(hr); while (TRUE) { // Free the previous row FreeProws (prowset); prowset = NULL; hr = ptbl->QueryRows (1, 0, &prowset); if ((hr != S_OK) || (prowset == NULL) || (prowset->cRows == 0)) break; CBR (prowset->aRow[0].cValues == spta.cValues); SPropValue *pval = prowset->aRow[0].lpProps; CBR (pval[0].ulPropTag == PR_DISPLAY_NAME); CBR (pval[1].ulPropTag == PR_ENTRYID); //Windows Mobile natively has 2 MAPI Message Stores: "ActiveSync" and "SMS" (then also POP\SMTP if user adds them) //Now we're interested on ActiveSync if (!_tcscmp(pval[0].Value.lpszW, TEXT("ActiveSync"))) { // Get the Message Store pointer hr = m_pSession->OpenMsgStore(0, pval[1].Value.bin.cb, (LPENTRYID)pval[1].Value.bin.lpb, 0, 0, &pStore); CHR(hr); //Invoke internal function to save and count messages from store's Inbox hr = SaveMessagesFromStore(pStore, g_pszFilename); CHR(hr); } } //finishing off... hr = m_pSession->Logoff(0, 0, 0); Exit: //make sure we don't leak memory FreeProws(prowset); RELEASE_OBJ(ptbl); RELEASE_OBJ(pStore); RELEASE_OBJ(m_pSession); MAPIUninitialize(); return g_ulNumberOfMessages; }
3- Now, thanks to IMsgStore::GetReceiveFolder, we can grab a pointer to the Inbox folder and invoke another internal function, which works directly on a specific MAPI Folder instead of a MAPI Store (I preferred to maintain them separated so that they can be easily ported to other scenarios -- i.e. for example the Drafts or the Sent Items folder):
////////////////////////////////////////////////////////////////////////////// //SaveMessagesFromStore //Accesses the passed MAPI Message Store and copies the items from the "Inbox" folder into a file (\mails.txt) HRESULT SaveMessagesFromStore(IMsgStore* pStore, LPCTSTR pszFilename) { HRESULT hr = E_FAIL; ULONG cbEntryId = 0; ULONG ulObjType = 0; LPMAPIFOLDER pFolder = NULL; LPENTRYID pEntryId = NULL; LPMAPITABLE ptbl = NULL; SRowSet *prowset = NULL; // Get the inbox folder hr = pStore->GetReceiveFolder(NULL, MAPI_UNICODE, &cbEntryId, &pEntryId, NULL); CHR(hr); // We have the entryid of the inbox folder, let's get the folder and messages in it hr = pStore->OpenEntry(cbEntryId, pEntryId, NULL, 0, &ulObjType, (LPUNKNOWN*)&pFolder); CHR(hr); //check CBR(ulObjType == MAPI_FOLDER); //invoke helper function to retrieve messages from MAPI Folder hr = SaveMessagesFromFolder(pFolder, pszFilename); CHR(hr); //update global variable so that SaveMessagesIntoFile() can return the number of the messages in Inbox hr = CountMessagesInFolder(pFolder, &g_ulNumberOfMessages); CHR(hr); //success! hr = S_OK; Exit: FreeProws (prowset); RELEASE_OBJ(pFolder); RELEASE_OBJ(ptbl); MAPIFreeBuffer(pEntryId); return hr; }
4- And this is the function doing the most of the job! A next version of the application may write XML-ish so that a NETCF client can easily use for example XmlTextReader or whatever (at the moment I didn't want wasting time on understanding how to include information containing the characters " and @...).
////////////////////////////////////////////////////////////////////////////// //SaveMessagesFromFolder //Accesses the passed MAPI Message Folder and copies the items into a file HRESULT SaveMessagesFromFolder(IMAPIFolder* pFolder, LPCTSTR pszFilename) { static const SizedSPropTagArray(4, spta) = {4, PR_ENTRYID, PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT, PR_MESSAGE_DELIVERY_TIME}; static const SizedSSortOrderSet(1, sortOrderSet) = { 1, 0, 0, { PR_MESSAGE_DELIVERY_TIME, TABLE_SORT_DESCEND } }; HRESULT hr = E_FAIL; IMAPITable * pContentsTable = NULL; SRowSet * psrs = NULL; IMessage * pMsg = NULL; CBR (pFolder != NULL); //Strings represeting From, To, Subject, Date\Time //(not Body, as it can be so large that it's better to write it directly to the file) LPTSTR pszSender = new TCHAR[MAXBUF]; ZeroMemory(pszSender, MAXBUF - 1); LPTSTR pszTo = new TCHAR[MAXBUF]; ZeroMemory(pszTo, MAXBUF - 1); LPTSTR pszSubject = new TCHAR[MAXBUF]; ZeroMemory(pszSubject, MAXBUF - 1); LPTSTR pszDate; // = new TCHAR[MAXBUF]; //ZeroMemory(pszDate, MAXBUF - 1); LPTSTR pszTime; // = new TCHAR[MAXBUF]; //ZeroMemory(pszTime, MAXBUF - 1); LPTSTR pszBody; //Buffer for writing to the file LPTSTR lpBuf = new TCHAR[MAXBUF]; ZeroMemory(lpBuf, MAXBUF - 1); // Open the folder's contents table hr = pFolder->GetContentsTable (MAPI_UNICODE, &pContentsTable); CHR(hr); // Sort the table that we obtained by time hr = pContentsTable->SortTable((SSortOrderSet *)&sortOrderSet, 0); CHR(hr); // Set columns we need (PR_ENTRYID, PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT, PR_MESSAGE_DELIVERY_TIME) hr = pContentsTable->SetColumns ((SPropTagArray *) &spta, 0); CHR(hr); //start writing file: \xFEFF is used so that the file can be opened by "special editors" hr = LogToFile(TEXT("\xFEFF"), g_pszFilename); // Iterate over each row of the Folder's contents (i.e. messages) table while (1) { // Get a row hr = pContentsTable->QueryRows (1, 0, &psrs); CHR(hr); // Did we hit the end of the table? if (psrs->cRows != 1) break; // Open this message hr = pFolder->OpenEntry (psrs[0].aRow[0].lpProps[0].Value.bin.cb, (ENTRYID *) psrs[0].aRow[0].lpProps[0].Value.bin.lpb, NULL, 0, NULL, (IUnknown **) &pMsg); CHR(hr); //check CBR (psrs[0].aRow[0].lpProps[0].ulPropTag == PR_ENTRYID); CBR (psrs[0].aRow[0].lpProps[1].ulPropTag == PR_SENDER_EMAIL_ADDRESS); CBR (psrs[0].aRow[0].lpProps[2].ulPropTag == PR_SUBJECT); CBR (psrs[0].aRow[0].lpProps[3].ulPropTag == PR_MESSAGE_DELIVERY_TIME); //FROM PR_SENDER_EMAIL_ADDRESS: //LPCTSTR pszSender = psrs[0].aRow[0].lpProps[1].Value.lpszW; hr = StringCchCopy(pszSender, MAXBUF, psrs[0].aRow[0].lpProps[1].Value.lpszW); CHR(hr); //FROM PR_SUBJECT: //LPCTSTR pszSubject = psrs[0].aRow[0].lpProps[2].Value.lpszW; hr = StringCchCopy(pszSubject, MAXBUF, psrs[0].aRow[0].lpProps[2].Value.lpszW); CHR(hr); //FROM PR_MESSAGE_DELIVERY_TIME: SYSTEMTIME st = {0}; FileTimeToSystemTime(&psrs[0].aRow[0].lpProps[3].Value.ft, &st); //FROM PR_MESSAGE_DELIVERY_TIME: //Date... int lenDate = GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, NULL, NULL, 0); pszDate = new TCHAR[lenDate]; //The count before includes the terminating null if (0 == GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, NULL, pszDate, lenDate)) { hr = E_FAIL; goto Exit; } //.. and Time: int lenTime = GetTimeFormat(LOCALE_USER_DEFAULT, TIME_FORCE24HOURFORMAT | TIME_NOTIMEMARKER, &st, NULL, NULL, 0); pszTime = new TCHAR[lenTime]; //The count before includes the terminating null if (0 == GetTimeFormat(LOCALE_USER_DEFAULT, TIME_FORCE24HOURFORMAT | TIME_NOTIMEMARKER, &st, NULL, pszTime, lenTime)) { hr = E_FAIL; goto Exit; } //RECIPIENTs are not a simple property: invoke helper function that internally uses GetRecipientTable() and PR_EMAIL_ADDRESS hr = WriteMessageRecipientsToString (pMsg, &pszTo); CHR(hr); //Mail's BODY can be so large that it's better to write BODY's data chunk directly to the file //instead of retrieving a LPTSTR representing it and then place it within a LPTSTR without knowing //the dimenions a priori hr = StringCchPrintf((LPTSTR)lpBuf, LocalSize(lpBuf) / sizeof(TCHAR), TEXT("=========================================================\r\n") TEXT("FROM:\t\t%s\r\n") //pszSender TEXT("SENT:\t\t%s, %s\r\n") //pszDate, pszTime TEXT("TO:\t\t%s\r\n") //pszTo TEXT("SUBJECT:\t%s\r\n") //pszSubject TEXT("\r\n"), pszSender, pszDate, pszTime, pszTo, pszSubject); CHR(hr); hr = LogToFile(lpBuf, pszFilename); CHR(hr); hr = WriteMessageBodyToFile(pMsg, g_pszFilename); CHR(hr); hr = LogToFile(TEXT("\r\n\r\n"), pszFilename); CHR(hr); // Clean up before re-using FreeProws (psrs); psrs = NULL; RELEASE_OBJ (pMsg); } //SUCCESS! hr = S_OK; Exit: FreeProws (psrs); RELEASE_OBJ (pMsg); RELEASE_OBJ (pContentsTable); DELETE_STR(lpBuf); DELETE_STR(pszSender); DELETE_STR(pszTo); DELETE_STR(pszSubject); DELETE_STR(pszDate); DELETE_STR(pszTime); return hr; }
5- Usually MAPI allow you to invoke a method that retrieves a IMAPITable associated to an object and then sets its columns accordingly to the properties you want to query: for example, you can use IMAPISession::GetMsgStoresTable to get the table of stores; another example is IMAPIFolder::GetContentsTable. To set the columns you can use a SPropTagArray structure. This is fine with properties you know don't require "too much" memory, such as PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT and PR_MESSAGE_DELIVERY_TIME; this is NOT true for properties like PR_BODY (or PR_BODY_HTML) where the object (in this case a string) can even reach hundreds of KB. In such a case, there's a different approach, based on the IMAPIProp::OpenProperty function. The function WriteMessageBodyToFile invoked by the above one shows the second approach (and btw on Windows Mobile only IAttach and IMessage expose such method).
Note the comments before the function's definition as they can explain why a MAPI-based code worked on WM5 and is no longer working on WM6: kudos to my friend David for them! Moreover, those comments explain that in order to have robust code working on different platforms you can query PR_MSG_STATUS to know if the message has a BODY in plain-text, or in HTML, or MIME text (whose parser is NOT included in this sample code).
////////////////////////////////////////////////////////////////////////////// //WriteMessageBodyToFile //Takes a IMessage as input and writes its BODY to a file /* NOTES: - For Windows Mobile 6: outgoing mail bodies in: * PR_BODY_W for plain text mail * PR_BODY_HTML_A (multibyte, for html mail) incoming mail bodies in: * ActiveSync account (both Exchange ActiveSync and PC Sync) ** PR_BODY_A, or ** PR_BODY_HTML_A * POP3/IMAP messages come in MIME format and we store the full MIME with body included in: PR_CE_MIME_TEXT Since MAPI specification does allow you to store info in different ways, to make a robust client app you basically need to loop through possible location of the properties until a match is found. You can query PR_MSG_STATUS and check against flags: * MSGSTATUS_HAS_PR_BODY * MSGSTATUS_HAS_PR_BODY_HTML * MSGSTATUS_HAS_PR_CE_MIME_TEXT ...to determine which properties are used for a given message. - For Windows Mobile 5.0: * incoming bodies are stored in PR_CE_MIME_TEXT (in multibyte) * outgoing mail bodies are stored in PR_BODY (in Unicode) * Everything is plain text. No HTML support */ HRESULT WriteMessageBodyToFile(IMessage * pMsg, LPCTSTR pszFileName) { HRESULT hr = E_FAIL; LPSTREAM pstmBody = NULL; BYTE* pszBodyInBytes; LPTSTR pszBody; IMAPIProp *pProp = NULL; ULONG rgTags[] = {2, PR_MSG_STATUS, PR_MESSAGE_FLAGS}; ULONG cValues = 0; SPropValue *rgMsgProps= NULL; //check CBR (pMsg != NULL); // Get an IMAPIProp Interface hr = pMsg->QueryInterface(IID_IMAPIProp, (LPVOID *) &pProp); CHR(hr); CPR(pProp); // Get the Message's STATUS and FLAGs properties (for Body we would only need STATUS, not FLAGs) hr = pProp->GetProps((LPSPropTagArray)rgTags, MAPI_UNICODE, &cValues, &rgMsgProps); CHR(hr); CBR(PR_MSG_STATUS == rgMsgProps[0].ulPropTag); CBR(PR_MESSAGE_FLAGS == rgMsgProps[1].ulPropTag); //the message can be MIME, HTML or PLAIN-TEXT: BOOL bIsMime = ( (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && ( (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_MIME_TEXT) || (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_CRYPT_MIME_TEXT) || (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_CE_SMIME_TEXT) ) ) && !(rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY) ; BOOL bHasBody = (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY); BOOL bHasHtmlBody = (rgMsgProps[0].ulPropTag == PR_MSG_STATUS) && (rgMsgProps[0].Value.ul & MSGSTATUS_HAS_PR_BODY_HTML); if (bHasBody) //PR_BODY, PR_BODY_W, PR_BODY_A { hr = pMsg->OpenProperty (PR_BODY, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); if (hr == MAPI_E_NOT_FOUND) { hr = pMsg->OpenProperty (PR_BODY_W, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); if (hr == MAPI_E_NOT_FOUND) { hr = pMsg->OpenProperty (PR_BODY_A, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); //CBR (hr != MAPI_E_NOT_FOUND); --> equivalent to the following if (hr == MAPI_E_NOT_FOUND) { //in this case no PR_BODY, nor PR_BODY_W, nor PR_BODY_A hr = E_FAIL; goto Exit; } } } //pstmBody now contains the body data stream, except for MIME } //PORTABILITY NOTE: Windows Mobile 5.0 doesn't have HTML support, so WM5's mapitags.h doesn't even define PR_BODY_HTML //In other words, the compiler complains about PR_BODY_HTML when targeting WM5 if the check about //_WIN32_WCE > 0x501 is not done #if _WIN32_WCE > 0x501 //we're on WM6, 6.1, ... if (bHasHtmlBody) //PR_BODY_HTML, PR_BODY_HTML_W, PR_BODY_HTML_A { hr = pMsg->OpenProperty (PR_BODY_HTML_A, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); if (hr == MAPI_E_NOT_FOUND) { hr = pMsg->OpenProperty (PR_BODY_HTML_W, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); if (hr == MAPI_E_NOT_FOUND) { hr = pMsg->OpenProperty (PR_BODY_HTML, NULL, STGM_READ, 0, (IUnknown **) &pstmBody); //CBR (hr != MAPI_E_NOT_FOUND); --> equivalent to the following if (hr == MAPI_E_NOT_FOUND) { //in this case no PR_BODY_HTML, nor PR_BODY_HTML_W, nor PR_BODY_HTML_A hr = E_FAIL; goto Exit; } } } //pstmBody now contains the body data stream, except for MIME } #endif //process pstmBody... (cases PR_BODY(_x) and PR_BODY_HTML(_x)) -- NOT IF MIME if (bHasBody || bHasHtmlBody) { STATSTG statstg; DWORD cbBody = 0; ULONG cbRead = 0; //Get the size of the body hr = pstmBody->Stat(&statstg, 0); CHR(hr); //Allocate a buffer for the stream cbBody = statstg.cbSize.LowPart; pszBodyInBytes = new BYTE[cbBody+1]; ZeroMemory(pszBodyInBytes, cbBody+1); //Read stream array (IStream::Read wants a BYTE* as 1st arg) hr = pstmBody->Read (pszBodyInBytes, cbBody, &cbRead); CHR(hr); //check CBR(0 != cbRead); CBR(cbBody == (int)cbRead); //Add trailing NULL pszBodyInBytes[cbBody+1] = '\0'; //convert byte array to WCHAR array to be consistent with the rest of the strings written to the file //Remember that for WM6, as documented in http://msdn.microsoft.com/en-us/library/bb446140.aspx, //we're here with PR_BODY_HTML_A in case of both incoming and outgoing mails of the ActiveSync message store pszBody = new TCHAR [cbBody+1]; //remember that cbBody==cbRead ZeroMemory(pszBody, cbBody+1); MultiByteToWideChar(CP_ACP, 0, (char*)pszBodyInBytes, cbRead, pszBody, cbBody+1); //write to file hr = LogToFile(pszBody, g_pszFilename); CHR(hr); } //MIME if (bIsMime) { //not implemented... but needed if you have to retrieve WM5 received messages hr = HRESULT(ERROR_NOT_SUPPORTED); goto Exit; } //Success! hr = S_OK; Exit: RELEASE_OBJ (pstmBody); RELEASE_OBJ (pProp); DELETE_STR (pszBody); DELETE_STR (pszBodyInBytes); return hr; }
6- There was another specific MAPI that was interesting to see in action here: IMessage::GetRecipientTable (to retrieve all the recipients of the mail and fill the file in the <TO> </TO> section):
////////////////////////////////////////////////////////////////////////////// //WriteMessageRecipientsToString //RECIPIENTs are not a simple property: you need MAPI GetRecipientTable() and PR_EMAIL_ADDRESS HRESULT WriteMessageRecipientsToString (IMessage* pMsg, LPTSTR* pszTo) { HRESULT hr = S_OK; IMAPITable * pRecipientTable = NULL; SRowSet * psrs = NULL; SPropValue * pspv = NULL; SPropValue * pspvLast = NULL; //this is used simply to add a ", " after the 1st recipient (in case of multiple recipients) BOOL fFirstRecipient = TRUE; size_t pcchA, pcchB; //reset passed variable size_t len; hr = StringCchLength(*pszTo, MAXBUF, &len); CHR(hr); ZeroMemory(*pszTo, len); //check CBR (pMsg != NULL); //Fill the table of Recipients hr = pMsg->GetRecipientTable (MAPI_UNICODE, &pRecipientTable); CHR(hr); while (1) { // Copy properties to the ADRLIST hr = pRecipientTable->QueryRows (1, 0, &psrs); CHR(hr); // Did we hit the end of the table? if (psrs->cRows != 1) break; // Point just past the last property pspvLast = psrs->aRow[0].lpProps + psrs->aRow[0].cValues; // Loop through all the properties returned for this row for (pspv = psrs->aRow[0].lpProps; pspv < pspvLast; ++pspv) { switch (pspv->ulPropTag) { //At this point you may also be interested on PR_DISPLAY_NAME case PR_EMAIL_ADDRESS: case PR_EMAIL_ADDRESS_A: if (!fFirstRecipient) { hr = StringCchLength(*pszTo, MAXBUF, &pcchA); CHR(hr); //hr = StringCchLength(TEXT(", "), STRSAFE_MAX_CCH, &pcchB); //This is =2 //CHR(hr); //hr = StringCchCat(pszRet, pcchA+2+1, TEXT(", ")); hr = StringCchCat(*pszTo, pcchA+2+1, TEXT(", ")); CHR(hr); } else { fFirstRecipient = FALSE; } hr = StringCchLength(*pszTo, MAXBUF, &pcchA); CHR(hr); hr = StringCchLength(pspv->Value.lpszW, MAXBUF, &pcchB); CHR(hr); hr = StringCchCat(*pszTo, pcchA+pcchB+1, pspv->Value.lpszW); CHR(hr); break; default: break; } } //Clen rows before re-using FreeProws (psrs); psrs = NULL; } //Success! hr = S_OK; Exit: FreeProws (psrs); RELEASE_OBJ (pRecipientTable); return hr; }
7- To be complete, the code needs a function that counts the number of the messages in a given folder:
//////////////////////////////////////////////////////////// // CountMessagesInFolder // Takes a folder as argument and fills out the # of messages in it HRESULT CountMessagesInFolder(LPMAPIFOLDER pFolder, ULONG * ulTotalMessages) { HRESULT hr = E_FAIL; ULONG rgTags[] = {2, PR_CONTENT_COUNT, PR_FOLDER_TYPE}; ULONG cValues = 0; IMAPIProp *pProp = NULL; SPropValue *rgFolderProps= NULL; // Get an IMAPIProp Interface hr = pFolder->QueryInterface(IID_IMAPIProp, (LPVOID *) &pProp); CHR(hr); CPR(pProp); // Get the Folder PR_CONTENT_COUNT property hr = pProp->GetProps((LPSPropTagArray)rgTags, MAPI_UNICODE, &cValues, &rgFolderProps); CHR(hr); CBR(PR_CONTENT_COUNT == rgFolderProps[0].ulPropTag); CBR(PR_FOLDER_TYPE == rgFolderProps[1].ulPropTag); //Set #messages *ulTotalMessages = rgFolderProps[0].Value.ul; hr = S_OK; Exit: RELEASE_OBJ(pProp); return hr; }
and another function that appends a string into a file:
//////////////////////////////////////////////////////////// // LogToFile // Writes szLog into the file named pszFilename HRESULT LogToFile(LPTSTR szLog, LPCTSTR pszFilename) { HRESULT hr = E_FAIL; //Open the handle to the file (and create it if it doesn't exist HANDLE hFile = CreateFile(pszFilename, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hFile) goto Exit; //Set the pointer at the end so that we can append szLog DWORD dwFilePointer = SetFilePointer(hFile, 0, NULL, FILE_END); if (0xFFFFFFFF == dwFilePointer) goto Exit; //Write to the file DWORD dwBytesWritten = 0; BOOL bWriteFileRet = WriteFile(hFile, szLog, wcslen(szLog) * 2, &dwBytesWritten, NULL); if (!bWriteFileRet) goto Exit; //Flush the buffer BOOL bFlushFileBuffersRet = FlushFileBuffers(hFile); if (!bFlushFileBuffersRet) goto Exit; //Success hr = S_OK; Exit: if (NULL != hFile) CloseHandle(hFile); return hr; }
For the sake of clarity, the macros I used were:
////////////////////////////////////////////////////////////////////////////// //Macros #define _ExitLabel Exit #define CHR(hResult) \ if(FAILED(hResult)) { hr = (hResult); goto _ExitLabel;} #define CPR(pPointer) \ if(NULL == (pPointer)) { hr = (E_OUTOFMEMORY); goto _ExitLabel;} #define CBR(fBool) \ if(!(fBool)) { hr = (E_FAIL); goto _ExitLabel;} #define RELEASE_OBJ(s) \ if (NULL != s) \ { \ s->Release(); \ s = NULL; \ } #define DELETE_STR(s) \ if (NULL != s) \ delete [] s;
NOTEs:
And now, enjoy your MAPI-Programming on Windows Mobile 6!!
Cheers,
~raffaele
Imagine you want to uninstall an application on a Windows Mobile device, and you want to do that through a CAB file. There are other means, yes, (see doc) however in this particular case a CAB was required because IT admins were using SCCM (System Center Configuration Manager) to handle mobile devices remotely and wanted a CAB. Ok, no problems: it's pure XML Provisioning! It's simply a matter of targeting the UnInstall CSP (Configuration Service Provider) by using for example the following XML and package it into a CAB through makecab.exe accordingly to the documentation (remember that .CAB and .CPF are the same, the only difference being with the outputted result: in the default Messaging account's inbox for .CPF and directly through a dialog for .CAB):
<wap-provisioningdoc> <characteristic type="UnInstall"> <characteristic type="AppName"> <parm name="uninstall" value="1"/> </characteristic> </characteristic> </wap-provisioningdoc>
The usual steps when creating a XML Provisioning-based approach involve testing the XML through RapiConfig: we did it and XML was fine (i.e. the application gets uninstalled). Then we packaged a CAB through makecab.exe as the doc dictates and run the CAB on the device: it returned a general installation failure message ("Installation of xx.cab was unsuccessful."). When troubleshooting the problem, we tried by compressing\not compressing the CAB (makecab.exe /D COMPRESS=OFF), then we made sure the problem was not with the long name of the CAB file (this was a problem many years ago, but no more), then with the Security Configuration and even with the formatting (ANSI\UTF-8) of the _setup.xml included in the CAB...
At the end it turned out that the problem is on Windows Mobile Devs' desk for further analysis in order to be addressed in future releases: simply put, the UnInstall CSP doesn't work when invoked through a CAB or CPF and there's simply no workaround apart from choosing a different approach to uninstall the application. If having an executable was acceptable, then we could simply run the following native code:
LPCWSTR g_wszUninstallAppNameXml = L"<wap-provisioningdoc> " L" <characteristic type=\"UnInstall\"> " L" <characteristic type=\"AppName\"> " L" <parm name=\"uninstall\" value=\"1\"/> " L" </characteristic> " L" </characteristic>" L"</wap-provisioningdoc>"; HRESULT UninstallAppName() { HRESULT hr = E_FAIL; LPWSTR wszOutput = NULL; // Process the XML. hr = DMProcessConfigXML(g_wszUninstallAppNameXml, CFGFLAG_PROCESS, &wszOutput); // The caller must delete the XML returned from DMProcessConfigXML. delete [] wszOutput; return hr; }
Or, for a managed approach:
string XmlUninst = "<wap-provisioningdoc>\r\n" + " <characteristic type=\"UnInstall\">\r\n" + " <characteristic type=\"AppName\">\r\n" + " <parm name=\"uninstall\" value=\"1\"/>\r\n" + " </characteristic>\r\n" + " </characteristic>\r\n" + "</wap-provisioningdoc>"; XmlDocument configDoc = new XmlDocument(); configDoc.LoadXml(XmlUninst); ConfigurationManager.ProcessConfiguration(configDoc, false);
However, as I said, in this case a CAB was required.
We came up with a simple workaround: create a CAB (through a Smart Device CAB project) and include a SETUP.DLL in it, whose Install_Init event invokes the DMProcessConfigFile with the XML Provisioning targeting the UnInstall CSP. The code for such SETUP.DLL was the following -- it's the usual one with simply a customized Install_Init:
HINSTANCE g_hinstModule; LPCWSTR g_wszUninstallAppNameXml = L"<wap-provisioningdoc> " L" <characteristic type=\"UnInstall\"> " L" <characteristic type=\"AppName\"> " L" <parm name=\"uninstall\" value=\"1\"/> " L" </characteristic> " L" </characteristic>" L"</wap-provisioningdoc>"; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: g_hinstModule = (HINSTANCE)hModule; break; } return TRUE; } // ************************************************************************** // Function Name: Install_Init // // Arguments: // IN HWND hwndParent handle to the parent window // IN BOOL fFirstCall indicates that this is the first time this function is being called // IN BOOL fPreviouslyInstalled indicates that the current application is already installed // IN LPCTSTR pszInstallDir name of the user-selected install directory of the application // // Return Values: // codeINSTALL_INIT // returns install status // // Description: // The Install_Init function is called before installation begins. // User will be prompted to confirm installation. // ************************************************************************** SETUP_API codeINSTALL_INIT Install_Init( HWND hwndParent, BOOL fFirstCall, BOOL fPreviouslyInstalled, LPCTSTR pszInstallDir ) { HRESULT hr = E_FAIL; LPWSTR wszOutput = NULL; // Process the XML. hr = DMProcessConfigXML(g_wszUninstallAppNameXml, CFGFLAG_PROCESS, &wszOutput); CHR(hr); Error: // The caller must delete the XML returned from DMProcessConfigXML. if (wszOutput != NULL) { delete [] wszOutput; } if (hr == S_OK) { return codeINSTALL_INIT_CONTINUE; } else { return codeINSTALL_INIT_CANCEL; } } // ************************************************************************** // Function Name: Install_Exit // // Purpose: processes the push message. // // Arguments: // IN HWND hwndParent handle to the parent window // IN LPCTSTR pszInstallDir name of the user-selected install directory of // the application // // Return Values: // returns install status: codeINSTALL_EXIT // ************************************************************************** SETUP_API codeINSTALL_EXIT Install_Exit( HWND hwndParent, LPCTSTR pszInstallDir, WORD cFailedDirs, WORD cFailedFiles, WORD cFailedRegKeys, WORD cFailedRegVals, WORD cFailedShortcuts ) { return codeINSTALL_EXIT_DONE; } // ************************************************************************** // Function Name: Uninstall_Init // // Arguments: // IN HWND hwndParent handle to the parent window // IN LPCTSTR pszInstallDir name of the user-selected install directory of the application // // Return Values: // returns uninstall status: codeUNINSTALL_INIT // // ************************************************************************** SETUP_API codeUNINSTALL_INIT Uninstall_Init( HWND hwndParent, LPCTSTR pszInstallDir ) { return codeUNINSTALL_INIT_CONTINUE; } // ************************************************************************** // Function Name: Uninstall_Exit // // Arguments: // IN HWND hwndParent handle to the parent window // // Return Values: // returns uninstall status: codeUNINSTALL_EXIT // // ************************************************************************** SETUP_API codeUNINSTALL_EXIT Uninstall_Exit( HWND hwndParent ) { return codeUNINSTALL_EXIT_DONE; }