Pocket Outlook: Empty Deleted Items folder on SERVER's Mailbox

- MAPI: the wrong approach (yet interesting sample code)

- WebDAV: DELETE command via HTTP

- Customize Pocket Outlook menu items

 

One of the features that Windows Mobile users frequently asked to the Product Group had always been the native ability to empty the Deleted Items folder on the server which the device is sync-ed to. Finally Windows Mobile 6-based devices have such ability when synchronizing with Exchange 2007 (Menu\Tools\Empty Deleted Items):

empty

The truth is that it's still plenty of Windows Mobile 5.0 devices (and Exchange 2003) in the real world, and many of their users are looking for a way to reach the goal... Smile The only available way seemed to be to access OWA from the device's web browser and here empty the folder: this is not acceptable in many cases for real-world scenarios. In other cases you may have found software to be installed server-side exposing such function to device-based clients. ... I've been working with some colleagues on this and I think it's worth mentioning here what we found...

 

- MAPI: the wrong approach (yet interesting sample code)

When started researching about the goal, I came across the MAPI (Messaging API) IMAPIFolder::DeleteMessages . Wow! Precisely the function I need, I said to myself. Unfortunately this wasn't true (BECAUSE I NEEDED A DEVICE-SIDE APPLICATION), but at least I spent some time on MAPI programming, which is really powerful after you  understand how it works... (and admittedly it's not really user-friendly...). So I ended up with an application that empties the Deleted Items folder, but only the one ON THE DEVICE, because you can't synchronize back it to the Exchange server. Interestingly, I hadn't to develop the whole code because portions of it were already available on the web (see for example "Practical use of MAPI") or in the WM6 SDK Samples (specifically, windows mobile 6 sdk\Samples\Common\CPP\Win32\InboxMenuExtensibility, which I'm going to talk about in a bit). Note that if you use the same MAPI on a server-side application (e.g. a Web Service exposing a Web Method that devices can invoke), then you would reach the goal: that's basically how the Blackberry Exchange-connector works, for example, when invoked from the device.

The working sample I came up with is the following: it's not thoughtfully tested and I can't say it's error-free (or that it's the best code to achieve the result), but it does what it was meant for -- empty the Deleted Items folder (in its case, ON THE DEVICE). I copied it to the end of the post.

 

- WebDAV: DELETE command via HTTP

As I said, since the Deleted Items folder is not sync-ed back and forth between server and device, the MAPI-based approach is not the correct one. The one that works, IF WEBDAV IS AN ALLOWED WEB SERVICE EXTENSION OF THE IIS Front-End Exchange SERVER, it's through WebDAV. The idea is very simple: allow a client application to access the mail-folders via HTTPS and send them some commands such as

    DELETE /pub2/folder1/folder2 HTTP/1.1
Host: hostname
Content-Length: 0
 

Basically I'm talking about an empty HTTP request (programmatically managed through a HttpWebRequest object, for example, in a .NET application) whose command is DELETE and whose argument is the path of the Deleted%20Items folder. There are many sample codes available at the doc Deleting Items (WebDAV) . I'm copying the C# code here to add few comments and to show how simple it is:

 using System;
using System.Net;

namespace ExchangeSDK.Snippets.CSharp
{
   class DeletingItemsWebDAV
   {
      [STAThread]
      static void Main(string[] args)
      {
         System.Net.HttpWebRequest Request;
         System.Net.WebResponse Response;
         System.Net.CredentialCache MyCredentialCache;
         string strSourceURI = "https://server/public/TestFolder1/test.txt";

         //In this sample user credentials are hard-coded for didactic purposes
         string strUserName = "UserName";
         string strPassword = "!Password";
         string strDomain = "Domain";

         try
         {
            // Create a new CredentialCache object and fill it with the network
            // credentials required to access the server. 
            // we won't pass the credentials in clear-text...
            MyCredentialCache = new System.Net.CredentialCache();
            MyCredentialCache.Add( new System.Uri(strSourceURI),
               "NTLM",
               new System.Net.NetworkCredential(strUserName, strPassword, strDomain)
               );

            // Create the HttpWebRequest object.
            Request = (System.Net.HttpWebRequest)HttpWebRequest.Create(strSourceURI);

            // Add the network credentials to the request.
            Request.Credentials = MyCredentialCache;

            // Specify the DELETE method.
            Request.Method = "DELETE";

            // Send the DELETE method request.
            Response = (System.Net.HttpWebResponse)Request.GetResponse();

            // Close the HttpWebResponse object.
            Response.Close();

            Console.WriteLine("Item successfully deleted.");

         }
         catch(Exception ex)
         {
            // Catch any exceptions. Any error codes from the DELETE
            // method request on the server will be caught here, also.
            Console.WriteLine(ex.Message);
         }
      }
   }
}

 

The power of WebDAV is that it's independent on the client, so for example you can run all your tests from a DESKTOP console. Moreover, to check if there's any server-side issue, you can use the Microsoft Download Microsoft Exchange Server Public Folder DAV-based Administration Tool.

Exchange servers' administrators may argue to us (mobile developers Nerd) that WebDAV is less secure. Let them read for example the doc at Authentication and Security Using WebDAV: it states that by using SSL, WebDAV is as secure as OWA.

 

- Customize Pocket Outlook menu items

Ok, now that you know how to achieve the goal, you may wonder how to integrate this new functionality on the system's Pocket Outlook (tmail.exe). It may be a bit tricky, but there's a complete NATIVE sample showing that in the SDK (so you'll need to use the native sample code of the Deleting Items (WebDAV) page): windows mobile 6 sdk\Samples\Common\CPP\Win32\InboxMenuExtensibility. This sample needs to be modified in some parts and may require a bit of knowledge on COM programming, but it's fully documented. It's basically a DLL COM Server loaded by tmail.exe (so it may require to be signed with a Priv Certificate), which ultimately invokes InsertMenu API to insert the menu item you want. The sample is more complex than strictly required in our case, as it also shows how to modify the menu items collection depending on the current context (i.e. if a message is selected or viewed).

 

Cheers!

~raffaele

 

P.S. The MAPI-based approach:

 #include "stdafx.h"
#include <windows.h>
#include <commctrl.h>

#include <initguid.h>
#include <pimstore.h>
#include <mapiutil.h>

HRESULT hr = E_FAIL;
LPMAPISESSION m_pSession;

////////////////////////////////////////////////////////////////////////////// 
//Functions declarations
HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList);
HRESULT DeleteMessages(IMsgStore* pStore);
HRESULT GetWastebasketForFolder(LPMAPIFOLDER pFolder, LPMAPIFOLDER* ppfldrWastebasket);
ULONG CountMessagesInFolder(LPMAPIFOLDER pFolder);
HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList);

////////////////////////////////////////////////////////////////////////////// 
//Macros
#define _ErrorLabel Error

#define CHR(hResult) \
    if(FAILED(hResult)) { hr = (hResult); goto _ErrorLabel;} 

#define CPR(pPointer) \
    if(NULL == (pPointer)) { hr = (E_OUTOFMEMORY); goto _ErrorLabel;} 

#define CBR(fBool) \
    if(!(fBool)) { hr = (E_FAIL); goto _ErrorLabel;} 

#define ARRAYSIZE(s) (sizeof(s) / sizeof(s[0]))

#define RELEASE_OBJ(s)  \
    if (NULL != s)      \
    {                   \
        s->Release();   \
        s = NULL;       \
    }


//////////////////////////////////////////////////////////////////////////////// 
//MAIN
int _tmain(int argc, _TCHAR* argv[])
{
    SRowSet *prowset = NULL;
    IMAPITable* ptbl = NULL;
    IMsgStore* pStore = NULL;

    // Warn user that the messages will be permanently deleting 
    int iResult = ::MessageBox(GetActiveWindow(), 
                TEXT("Are you sure you want to permanently delete all messages in Deleted Items ON THE DEVICE?"), 
                TEXT("Permanently Delete Items"), MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2);

    // If IDYES - then continue / else exit method now
    hr = (iResult == IDYES) ? S_OK : E_FAIL;                        
    CHR(hr);

    //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);

    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;
 
        ASSERT (prowset->aRow[0].cValues == spta.cValues);
        SPropValue *pval = prowset->aRow[0].lpProps;
 
        ASSERT (pval[0].ulPropTag == PR_DISPLAY_NAME);
        ASSERT (pval[1].ulPropTag == PR_ENTRYID);
 
        //Windows Movile has 2 MAPI Message Stores: "ActiveSync" and "SMS"
        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);
 
            //Empty Deleted Items folder
            hr = EmptyDeletedMessages(pStore);
            CHR(hr);
        }
    }

    //finishing off...
    hr = m_pSession->Logoff(0, 0, 0);
    
Error:
    //make sure we don't leak memory
    FreeProws(prowset);
    RELEASE_OBJ(ptbl);
    RELEASE_OBJ(pStore);
    RELEASE_OBJ(m_pSession);   

    MAPIUninitialize();

    return hr;
}


///////////////////////////////////////////////////////////////////////////////
// EmptyDeletedMessages - Takes the Message Store as argument and empties its 
//  Deleted Items folder
HRESULT EmptyDeletedMessages(IMsgStore* pStore)
{
    hr = E_FAIL;
    ULONG cMsgCount = 0;
    IMAPIFolder* pFolder = NULL;
    IMAPIFolder* pWasteBasket = NULL;
    IMAPITable *pTable = NULL; 
    ULONG cbEntryId = 0;
    ENTRYID *pEntryId = NULL;
    ULONG ulObjType = 0;
    SRowSet *pRows = NULL;
    LPENTRYLIST lpmsgEntryList = NULL;

    // First retrieve the ENTRYID of the Inbox folder of the message store
    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);
 
    // Be sure it's a folder
    ASSERT(ulObjType == MAPI_FOLDER);

    // Get the Deleted Items Folder (because it may be that current view is on wahtever folder
    hr = GetWastebasketForFolder(pFolder, &pWasteBasket);
    CHR(hr);
    CPR(pWasteBasket);

    // Get contents of the folder
    pWasteBasket->GetContentsTable(MAPI_UNICODE, &pTable);

    // We only care about the EntryID. 
    SizedSPropTagArray (1, spta) = {1, PR_ENTRYID};
    pTable->SetColumns((SPropTagArray *)&spta, 0);

    BOOL done = FALSE;

    // We need to know how many messages there are
    cMsgCount = CountMessagesInFolder(pWasteBasket);
    
    while(cMsgCount > 0)
    {
        // 10 is the max number of rows which QueryRows can return
        int cCurrentQuery = 0;

        if (cMsgCount > 10)
        {
            cCurrentQuery = 10;
        }
        else
        {
            cCurrentQuery = cMsgCount;
        }
        
        hr = pTable->QueryRows(cCurrentQuery, 0, &pRows);
        CHR(hr);
        
        // Did we hit the end of the table?
        if (pRows->cRows == 0)
        {   
            break;
        }

        // Since we are in a loop here and we are re-using lpmsgEntryList 
        // we may need to free the memory from the previous iteration first
        // In the case of the last iteration or Error - this will be freed at Error:
        MAPIFreeBuffer(lpmsgEntryList);
        lpmsgEntryList = NULL;
        
        // Otherwise - Get the List of EntryIDs from the RowSet as an EntryList
        //  Note: the resulting will EntryList be dependant on the RowSet so don't free until later
        hr = CreateEntryList(pRows, &lpmsgEntryList);
        CHR(hr);  


        // permanently delete the items 
        hr = pWasteBasket->DeleteMessages(lpmsgEntryList, NULL, NULL, NULL);
        CHR(hr);           
        
        cMsgCount -= pRows->cRows;
    }  

Error:
    RELEASE_OBJ(pFolder);
    RELEASE_OBJ(pWasteBasket);
    RELEASE_OBJ(pTable);
    FreeProws(pRows);
    
    MAPIFreeBuffer(pEntryId);
    
    return hr;
}


////////////////////////////////////////////////////////////
// CountMessagesInFolder - takes a folder as argument and returns 
//  the # of messages in it
ULONG CountMessagesInFolder(LPMAPIFOLDER pFolder)
{
    hr = E_FAIL;
    IMAPIProp *pProp = NULL; 
    ULONG rgTags[] = {2, PR_CONTENT_COUNT, PR_FOLDER_TYPE};
    ULONG cValues = 0;
    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);            
    
Error:
    // return the COUNT of messages in this folder
    return rgFolderProps[0].Value.ul;
}


////////////////////////////////////////////////////////////
// GetWastebasketForFolder
//  Independently on the currently selected folder, it returns 
//  the related Deleted Items folder
HRESULT GetWastebasketForFolder(LPMAPIFOLDER pFolder, LPMAPIFOLDER* ppfldrWastebasket)
{
    hr = E_FAIL;
    IMsgStore* pms = NULL;
    ULONG cItems;
    ULONG rgtagsFldr[] = { 1, PR_OWN_STORE_ENTRYID };
    ULONG rgtagsMsgStore[] = { 1, PR_IPM_WASTEBASKET_ENTRYID };
    LPSPropValue rgprops = NULL;

    // This method assumes that the CALLER already logged on to a MAPISession
    if (!m_pSession)
        CHR(E_FAIL);  

    // Now request the PR_OWN_STORE_ENTRYID on the folder.  This is the
    // ENTRYID of the message store that owns the folder object.
    hr = pFolder->GetProps((LPSPropTagArray)rgtagsFldr, MAPI_UNICODE, &cItems, &rgprops);
    CHR(hr);

    CBR(PR_OWN_STORE_ENTRYID == rgprops[0].ulPropTag);

    // Now open the message store object.
    hr = m_pSession->OpenEntry(rgprops[0].Value.bin.cb,
            (LPENTRYID)rgprops[0].Value.bin.lpb,
            NULL, 0, NULL, (LPUNKNOWN*)&pms);
    CHR(hr);

    MAPIFreeBuffer(rgprops);
    rgprops = NULL;

    // Get the ENTRYID of the wastebasket for the message store
    hr = pms->GetProps((LPSPropTagArray)rgtagsMsgStore, MAPI_UNICODE, &cItems, &rgprops);
    CHR(hr);

    // Now open the correct wastebasket and return it to the caller.
    CBR(PR_IPM_WASTEBASKET_ENTRYID == rgprops[0].ulPropTag);

    hr = m_pSession->OpenEntry(rgprops[0].Value.bin.cb,
            (LPENTRYID)rgprops[0].Value.bin.lpb,
            NULL, 0, NULL, (LPUNKNOWN*)ppfldrWastebasket);
    CHR(hr);

Error:
    MAPIFreeBuffer(rgprops);
    RELEASE_OBJ(pms);

    return hr;
}


////////////////////////////////////////////////////////////
// CreateEntryList
//  Needed because IMAPIFolder::DeleteMessages works on EntryLists
HRESULT CreateEntryList(SRowSet *pRows, ENTRYLIST **ppList)
{
    hr = E_FAIL;
    ENTRYLIST* plist = NULL;
    
    // How much space do we need to create this entry list?
    ULONG cbNeeded = sizeof(SBinaryArray) + (pRows->cRows * (sizeof(SBinary)));

    // Allocate one buffer to hold all the data for the list.
    hr = MAPIAllocateBuffer(cbNeeded, (LPVOID*)&plist);
    CHR(hr);
    CPR(plist);

    // Set the number of items in the EntryList 
    plist->cValues = pRows->cRows;

    // Set plist->lpbin to the place in the buffer where the array items will be
    // filled in
    BYTE* pb;
    pb = (BYTE*)plist;
    pb += sizeof(SBinaryArray);
    plist->lpbin = (SBinary*) pb;

    // Loop through the list setting the contents of the EntryList to the contents
    // of the incoming SRowSet
    for (int cItems = 0; cItems < (int)plist->cValues; cItems++)
    {
        plist->lpbin[cItems].cb = pRows->aRow[cItems].lpProps[0].Value.bin.cb;
        plist->lpbin[cItems].lpb = pRows->aRow[cItems].lpProps[0].Value.bin.lpb;

        // Track our memory usage 
        pb += sizeof(SBinary);
    }

    // Make sure that we didn't write off the end of our buffer...
    CBR(pb <= ((BYTE*)plist + cbNeeded));

Error:

    if (FAILED(hr))
    {
        MAPIFreeBuffer(plist);
        plist = NULL;
    }

    *ppList = plist;

    return hr;
}