Mobile Development - 'Support Side Story'

Broadcasting technical support to Windows Mobile\CE Application Developers to help realizing their potential

Broadcasting technical support to Windows Mobile\CE Application Developers to help realizing their potential

September, 2008

  • Mobile Development - 'Support Side Story'

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

    • 7 Comments

    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 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:

    • 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

  • Mobile Development - 'Support Side Story'

    Provisioning the UnInstall Configuration Service Provider fails if invoked through CAB or CPF

    • 2 Comments

    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! Nerd 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;
    }

     

     

    Cheers,

    ~raffaele

Page 1 of 1 (2 items)