A .NET Managed Windows Address Book (WAB) Wrapper

Download VS2005 Source Code
Download VS2005 Debug Binary

The Windows Address Book has to be one of the most convoluted components of Windows I’ve had to deal with. Here’s a free source .NET C++ wrapper I’ve written to make life a lot easier for everyone.

If you don't care about how this component was put together, I encourage you to download the pre-compiled AddressBook.dll and start playing. If you want to review the source code please do so, but please share your enhancements/fixes by sending me an email. Most methods in the source are commented and fairly simple, so I won't go into the boring details here. What I will cover however, are the precise steps I had to take in order to List, Create, Edit and Delete contacts stored in the WAB.

Listing Contacts in the WAB

Exposed in the managed WAB wrapper is a Contacts property which consists of a ContactCollection class. The meat of the extraction takes place within the WAB::AddressBook::GetContacts() method – let's take a look.

//
        // This is our .NET managed collection which will
        
        // hold all of the Contacts.
        //WAB::ContactCollection* ar = new WAB::ContactCollection;HRESULT hRes;
HINSTANCE
hinstLib;
fWABOpen procWABOpen;

// Holds our
    
    IAddrBook objectLPADRBOOK lpAdrBook;

// Allows us to free allocated memory throughout all WAB callsLPWABOBJECT lpWABObject;

// Doesn't do anything, but a DWORD is required for the last
    param
    // when instantiating
    
    the Address Book interface IAddrBook.DWORD Reserved2 =
NULL;

// Attempt to open the WAB32 libraryhinstLib = LoadLibrary("C:\\Program Files\\Common Files\\System\\wab32");

As stated in the comments, ContactCollection is just our .NET managed collection which will hold all of the Contacts. After instantiating, we're just loading the WAB32 library - nothing fancy going on here. A better implementation will be to obtain the wab32 path from the registry, I may modify the source later on to do this.

if (hinstLib != NULL)
{
  procWABOpen = (fWABOpen) GetProcAddress(hinstLib, "WABOpen");

  if (procWABOpen != NULL)  {
    try    {
      WAB_PARAM wp = {0};
      wp.cbSize = sizeof(WAB_PARAM);

      // Only want to deal with
    "Main Identity's Contacts"      wp.ulFlags = WAB_ENABLE_PROFILES;

      // Creates an instance
    of our IAddrBook interface
          // which will give us access to the WAB.      hRes = (procWABOpen)(&lpAdrBook,&lpWABObject, &wp, Reserved2);
    }
    catch(System::Exception* x)    {
      System::Diagnostics::Debug::WriteLine(x->ToString());
      throw new System::Exception(String::Concat("Unable to initialise WAB library. ",         x->ToString()));
    }

    if (hRes != S_OK)     {
     throw new System::Exception(String::Concat("WAB::AddressBook::GetAddresses(): procWABOpen failed with error code ", hRes.ToString()));    }

This is more of our initialisation of the WAB - again nothing to really explain. You can think of the WAB as a database. Everything is stored in rows with a binary Entry Indentifier (ENTRYID) for each row. In the next portion, lpcbEntryID tells us the size of what the ENTRYID is going to be and lpAdrBook->GetPAB() will return the Entry Identifier to our actual Address Book data.

    ULONG lpcbEntryID;
    ENTRYID *lpEntryID;

    hRes = lpAdrBook->GetPAB(
     &lpcbEntryID,
     &lpEntryID);

    if (hRes != S_OK)    {
      throw new System::Exception(       String::Concat("WAB::AddressBook::SaveContact(): lpAdrBook->GetPAB failed with error code ", hRes.ToString()));
    }

    LPUNKNOWN lpUnk = NULL;

LPUNKNOWN is going to hold a "container" which lets us get the contents of the WAB table. Next we're going to open the Address Book with MAPI_BEST_ACCESS which opens the entry with the best available access rights.

    ULONG ulFlags = MAPI_BEST_ACCESS;
    ULONG ulObjType = NULL;

    hRes = lpAdrBook->OpenEntry(
      lpcbEntryID,
      lpEntryID,
      NULL,
      ulFlags,
      &ulObjType,
      &lpUnk);

    ulFlags = NULL; 

MAPI_ABCONT is our Address Book Container. Obviously since we opened the IAddrBook, if the object returned isn't the Address Book Container then something is very wrong.

if (ulObjType == MAPI_ABCONT){
  //
      // Cast our "IUnknown" lpUnk that the lpAdrBook->OpenEntry
      // created, into a usable IABContainer interface.
      //
  IABContainer *lpContainer = static_cast <IABContainer *>(lpUnk);  LPMAPITABLE lpTable = NULL;

  //
      // Obtain the Table (IABTable interface) for the WAB. It
      // will let us query the WAB in a moment.
      //  hRes = lpContainer->GetContentsTable(
    ulFlags,
    &lpTable);

  //
      // Grab the number of rows in the table.
      //
  ULONG ulRows;
  hRes = lpTable->GetRowCount(0,&ulRows); 

SRowSet is a structure which contains an array of all of the actual rows. Each row will return just the bare minimum information regarding each contact; the EntryID, Name, Email address etc.

We will want to grab more than just the basics, so all we're going to use out of these rows are the ENRTYIDs. We will pull their specific details (properties) in a moment.

  SRowSet *lpRows;
  //
      // We can now query the WAB using the container.
      //

  hRes = lpTable->QueryRows(
    ulRows, // Get all Rows    0,
    &lpRows);

Next we're going to iterate through all of the rows in the table.

  for (ULONG i=0;i<lpRows->cRows;i++)  {
    SRow *lpRow = &lpRows->aRow[i];

    //
        // The IMailUser interface will let us extract
        // ALL of the information about the contact.
        //
    LPMAILUSER lpMailUser = NULL;
    LPSPropValue v2;
    int cb2;    ULONG cb;
    ENTRYID* entryID;
    LPBYTE lpb; 

Since we have the "basic" row containing just the contact's name and Entry ID. We now need to iterate through the row's set of values and obtain the Entry ID.

    for (ULONG x=0; x<lpRow->cValues; x++)     {
      SPropValue *lpProp = &lpRow->lpProps[x];
      if (lpProp->ulPropTag == PR_ENTRYID)      {
        lpb = lpProp->Value.bin.lpb;
        entryID = (ENTRYID*)lpProp->Value.bin.lpb;
        cb = lpProp->Value.bin.cb;
        break;      }
    }

Now that we have the Contact's Entry ID, we can pull the IMailUser interface from the Address Book.

    hRes = lpAdrBook->OpenEntry(
    cb,
    entryID,
    NULL,
    0,
    &ulObjType,
    (LPUNKNOWN *)&lpMailUser);

    if (hRes != S_OK)      
continue; 

When querying GetProps on the IMailUser with NULL specified as the SPropTagArray parameter, all of the details will be returned. v2 is an LSPropValue, this contains a Value param which has all of the actual string, FILETIME, binary and other data that represents each contact property.

    hRes = lpMailUser->GetProps( NULL, NULL, (ULONG*)&cb2, &v2 );

    //
        // If GetProps (read "Get Properties") failed
        // just skip it - something must be whack.
        //
    if (hRes != S_OK)      
continue; 

We now create an instance of our sexy managed Contact and Iterate through all of the properties calling SetManagedValue() to apply/convert them into the correct attributes of the Contact class. I'm using a custom attribute which lets you make a public property and assign it a ULONG value which corresponds with the list of available properties as is defined in the wabdefs.h file.

    WAB::Contact * adr = new WAB::Contact;
    adr->IsNew = false ;
    for ( int x=0;x<cb2;x++)    {
      SetManagedValue(adr, v2[x].ulPropTag, v2[x]);
    }



    // Add the managed contact to our
    ContactCollection    ar->Add(adr); 

Now we just need to clean up and return the managed contact collection.

    lpMailUser->Release();

    // Release the Row    lpWABObject->FreeBuffer(lpRow);
    lpRow = 0;

    // Release EntryID (~Thanks Todd!)    lpWABObject->FreeBuffer(entryID);
    entryID = 0;
  }

  // Release the row collection  lpWABObject->FreeBuffer(lpRows);
 }

 // Release each row's property collection
 for(ULONG i=0;i<lpRows->cRows;i++)  lpWABObject->FreeBuffer(lpRows->aRow[i].lpProps);

 // Release the row collection
     lpWABObject->FreeBuffer(lpRows); lpRows = 0;
 
  if (lpContainer)    lpContainer->Release();

  if (lpTable)    lpTable->Release();
  }
    
 // Release the lpEntry
 if (lpEntryID) {
   lpWABObject->FreeBuffer(lpEntryID);
   lpEntryID = 0;
 }
}

// Release the Address Book
    if (lpAdrBook) {lpAdrBook->Release();
lpAdrBook = 0;

// Release the WABObject (~Thanks Todd!)
if (lpWABObject) { lpWABObject->Release(); lpWABObject = 0;
}

FreeLibrary(hinstLib);

// Return our final managed collectionreturn ar;}

 

Creating and Modifying Contacts

WAB::Contact::SaveContact(WAB::Contact* c) is the starting point where we create a brand spanking new WAB entry, or modify an existing one. This method starts off pretty much identical to GetContacts(), so I won't bother repeating myself -- for consistency purposes, here's our WAB initialisation code:

//
        // Open up the WAB
        //HRESULT hRes;
HINSTANCE hinstLib;
fWABOpen
procWABOpen;

// Holds our IAddrBook objectLPADRBOOK lpAdrBook;

// Allows us to free allocated memory throughout all WAB callsLPWABOBJECT
lpWABObject;

// Doesn't do anything, but a DWORD is required for the last
    param
    // when instantiating the Address Book interface IAddrBook.DWORD Reserved2 = NULL;

// Attempt to open the WAB32 libraryhinstLib = LoadLibrary("C:\\Program
Files\\Common Files\\System\\wab32");

if (hinstLib != NULL){
  procWABOpen = (fWABOpen) GetProcAddress(hinstLib, "WABOpen");
  if (procWABOpen != NULL)  {
    try    {
      WAB_PARAM wp = {0};
      wp.cbSize = sizeof(WAB_PARAM);
      // Only want to deal with
    "Main Identity's Contacts" 
      wp.ulFlags = WAB_ENABLE_PROFILES;


      // Creates an instance
    of our IAddrBook interface
          // which will give us access to the WAB.      hRes = (procWABOpen)(&lpAdrBook,&lpWABObject, &wp, Reserved2);
    }
    catch(System::Exception* x)    {
      System::Diagnostics::Debug::WriteLine(x->ToString());
      throw new System::Exception(String::Concat("Unable to initialise WAB library. ", x->ToString()));    }

    if (hRes != S_OK)     {
      throw new System::Exception(        String::Concat("WAB::AddressBook::GetAddresses(): procWABOpen failed with error code ",
          hRes.ToString()));
    }

    ULONG lpcbEntryID;
    ENTRYID *lpEntryID;
    hRes = lpAdrBook->GetPAB(
    &lpcbEntryID,
    &lpEntryID);

    if (hRes != S_OK)     {
      throw new System::Exception(        String::Concat("WAB::AddressBook::SaveContact(): lpAdrBook->GetPAB failed with error code ",
          hRes.ToString()));
    }

BIG RED TRUCK: The key to modifying anything in the WAB is to always specify the MAPI_MODIFY flag when opening anything. Otherwise you will obviously get Access Denied messages.

  ULONG ulFlags = MAPI_MODIFY;
  ULONG ulObjType = NULL;
  LPUNKNOWN lpUnk = NULL;

  hRes = lpAdrBook->OpenEntry(
  lpcbEntryID,
  lpEntryID,
  NULL,
  ulFlags,
  &ulObjType,
  &lpUnk);

  if (ulObjType == MAPI_ABCONT)  {
    IABContainer *lpContainer = static_cast <IABContainer *>(lpUnk);    LPMAPITABLE lpTable = NULL;
    hRes = lpContainer->GetContentsTable(
    ulFlags,
    &lpTable);

    ULONG ulRows;
    hRes = lpTable->GetRowCount(0,&ulRows);

    SRowSet *lpRows;
    hRes = lpTable->QueryRows(
    ulRows, // Get all Rows
    0,
    &lpRows);

lpMailUser will hold the user that we're going to save data into

    LPMAILUSER lpMailUser = NULL;

If the contact already exists in the WAB (i.e. we're attempting to Modify it), we will first attempt to locate the IMailUser which corresponds to the managed Contact that we're saving.

When saving an existing contact, we use the ptaEID tag array for querying the IMailUser later on. This will allow us to only return the Email Address and Display Name which will act as our Unique Identifier.

  if (!contact->IsNew)  {

    static const SizedSPropTagArray(2,ptaEID) = { 2, { PR_EMAIL_ADDRESS, PR_DISPLAY_NAME } };
    bool found = 0;    
for(ULONG i=0;i<lpRows->cRows;i++)    {
      SRow *lpRow = &lpRows->aRow[i];

      LPSPropValue v2;
      int cb2;      ULONG cb;

      ENTRYID* entryID;
      int found = 0;
      for(ULONG x=0; x<lpRow->cValues; x++)      {
        SPropValue *lpProp = &lpRow->lpProps[x];
        if (lpProp->ulPropTag == PR_ENTRYID)         {
          entryID = (ENTRYID*)lpProp->Value.bin.lpb;
          cb = lpProp->Value.bin.cb;
          found = 1;
          
    break;        }
      }

      lpMailUser = NULL;

      // Open the mail user      hRes = lpAdrBook->OpenEntry(
      cb,
      entryID,
      NULL,
      ulFlags,
      &ulObjType,
      (LPUNKNOWN *)&lpMailUser);

      if (hRes != S_OK)        
continue;

Now we're going to get the PR_EMAIL_ADDRESS and PR_DISPLAY_NAME properties so that we can compare them against our managed Contact's CachedPrimaryEmail and CachedDisplayName properties - these act as our unique identifier.

We need to verify that the LSPropValue's each haven't returned the error code 0x8004010F, which means "The operation failed. An object could not be found." Simply trying to check for an s_OK against the hRes from GetProps() is no good because this will return a failure if the contact that you're editing does not have any email addresses.

      // Get the PR_EMAIL_ADDRESS
        and PR_DISPLAY_NAME properties      hRes = lpMailUser->GetProps( (LPSPropTagArray)&ptaEID, 0, (ULONG*)&cb2, &v2 );

      //
      //Compare the primary email address and display name as a "Unique Identifier"
      //
      System::String* email = NULL;
      System::String* name = NULL;

      if (v2[0].Value.err!=0x8004010F) // The operation failed. An object could not be found        email = new System::String( v2[0].Value.lpszA );
      if (v2[1].Value.err!=0x8004010F) // The operation failed. An object could not be found        name = new System::String( v2[1].Value.lpszA );
      System::String* originalEmail = contact->CachedPrimaryEmail;
      System::String* originalName = contact->CachedDisplayName;

      // Free the row      lpWABObject->FreeBuffer(lpRow);

Now we simply try and match the contact's value against the managed Contact. StringsMatch() is a little helper method I created which will allow you to safely compare two System::Strings, even if one of the strings are NULL.

      //
              // Look for a match
              //      
if (!StringsMatch(originalEmail, email)       ||!StringsMatch(originalName, name))
      {
        lpMailUser->Release();
        continue;
      }

      // Found the IMailUser
    that we need to modify      found = 1;
      break;    }

    // If the existing user couldn't be
    found, then we
        // have a problem.    if (found)    {
      lpMailUser->Release();
      FreeLibrary(hinstLib);
      throw new System::Exception("Unable to save user because the Contact's cached DisplayName and Primary email identifiers don't match anything in the WAB.");
    }
  }
  else  {

If the contact is a new entry, then we need to perform a PR_DEF_CREATE_MAILUSER opperation. We will use ptaCreate to hold the "create" SPropTagArray. The PR_DEF_CREATE_MAILUSER flag tells our container to create a blank row/entry. It will return an LPSPropValue which contains the ENTRYID for us to use when creating our new IMailUser.

    static const SizedSPropTagArray(1, ptaCreate) = { 1, { PR_DEF_CREATE_MAILUSER } };
    ULONG cProps = 0;
    LPSPropValue dProps;

    // Create the entry
    

    hRes = lpContainer->GetProps(
    (LPSPropTagArray)&ptaCreate,
    0,
    &cProps,
    &dProps);

    if (HR_FAILED(hRes))    {
      FreeLibrary(hinstLib);
      throw new System::Exception(        String::Concat("WAB::AddressBook::SaveContact(): pContainer->GetProps containing flag PR_DEF_CREATE_MAILUSER failed with error code ", hRes.ToString()));
    }

With our blank row obtained, we can tell the container to create a new IMailUser.

    hRes = lpContainer->CreateEntry(
    dProps->Value.bin.cb,
    (LPENTRYID)dProps->Value.bin.lpb,
    CREATE_CHECK_DUP_LOOSE,
    (LPMAPIPROP*)&lpMailUser);

    if (HR_FAILED(hRes))    {
      FreeLibrary(hinstLib);
      throw new System::Exception(System::String::Concat("Unable to create IMailUser, lpContainer->CreateEntry() returned code ", hRes.ToString()));    }
  }

Now that we have a valid IMailUser object, all that we have to do is throw properties into it and call SaveChanges(). I wrote a helper method which iterates through all of the managed properties in a WAB::Contact class. The meat of writing a contact's properties within the WAB is that we simply perform the following code repeatedly for each property that we want to save:

SPropValue Prop;
Prop.ulPropTag = tag;
Prop.Value.XXXX = MyValue;lpMailUser->SetProps(1, &Prop,
NULL); 

Getting back to the example code, here is our final IMailUser write opperation:

  //
          // Call SetUnmanagedProperies() so that all of the
          // Contact's attributes are written to the IMailUser,
          // then all's we have to do is call SaveChanges()
          //  SetUnmanagedProperies(contact, lpMailUser);

  // Tell the IMailUser to save our work.  hRes = lpMailUser->SaveChanges(FORCE_SAVE);

  if (hRes != S_OK)  {
    lpMailUser->Release();
    lpWABObject->FreeBuffer(lpRows);
    throw new System::Exception(System::String::Concat("lpMailUser->SaveChanges(FORCE_SAVE) failed with returned code ", hRes.ToString()));  }
  //
      // Update the cached name and email address (which we use
      // as the unique identifier.
      //  contact->CachedDisplayName = contact->DisplayName;
  // Contact is no longer new  contact->IsNew = false;

And finally we can clean up everything.

    lpMailUser->Release();
    lpWABObject->FreeBuffer(lpRows);
  }
  
// Release the lpEntryID
       if (lpEntryID)   {
     lpWABObject->FreeBuffer(lpEntryID);
     lpEntryID = 0;
   }
  }
 }

  if (lpWABObject) { lpWABObject->Release(); lpWABObject = 0; }
  if (lpAdrBook) {lpAdrBook->Release (); lpAdrBook = 0;}  FreeLibrary(hinstLib);
  return true;}

Deleting Contacts from the WAB

The method which performs our delete opperation is WAB::Contact::DeleteContact(WAB::Contact* contact). Rather than repeating a lot of the WAB initialisation code yet again, I will just cut to the chase and show you what we do after obtaining the matching IMailUser which corresponds to the managed Contact that we want to delete:

// Find a match 
        
        if (!StringsMatch(originalEmail, email) ||!StringsMatch(originalName, name)){
  lpWABObject->FreeBuffer(lpRow);
    continue;
}

// Got our match, we don't need the IMailUser, since we're
    // using the ENTRYID data that we obtained earlier.
    

lpMailUser->Release();

LPENTRYLIST il = (LPENTRYLIST)malloc(sizeof(LPENTRYLIST));
il->cValues = 1;
il->lpbin = (SBinary*)malloc(sizeof(SBinary));
il->lpbin->cb = cb;
il->lpbin->lpb = lpb;

hRes = lpContainer->DeleteEntries(il, 0);

lpWABObject->FreeBuffer(lpRow);
lpWABObject->FreeBuffer(lpRows);
FreeLibrary(hinstLib);
return true;

And there you have it! The mysteries and confusion of the WAB explained and encapsulated into a simple managed layer -- enjoy.