MAPI on Windows Mobile 6: Programmatically retrieve mail BODY (sample code)

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. Smile 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... Nerd

 

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! Smile 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 https://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:

  • an application invoking MAPI on a client (a device in our case) accesses the local Message Stores of the client. So, if you set a "Download Size Limit" for the mails sync-ed with a backend Exchange server, then be prepared to retrieve PR_BODY and PR_BODY_HTML_A till that limit! In my case I wasn't really understanding why the code above was producing truncated bodies... but thanks to a friend I understood: incredibly simply, the message was not fully downloaded! Confused
  • There's not much error-tracking or tracing in the sample as it's meant to be a mere proof of concept with didactic goals: probably the best approach would be to modify the MACROs so that you can trace\log the function producing the problem, if so.
  • Remember to release the resources to avoid memorey leaks!!

 

And now, enjoy your MAPI-Programming on Windows Mobile 6!! Open-mouthed 

 

Cheers,

~raffaele