Suppose, for a moment, that your company has a public
Outlook folder that contains a bunch of contacts, and you want to synchronize
your local contacts folder with this public folder, maybe so you could
conveniently synchronize your PDA as well.
But you don’t want all of the contacts in the public folder, just
certain ones.
Furthermore, you want to
keep this list up-to-date with a push of a button.
How would you go about it?
You might think this is a simple problem, but the
particulars of your setup might require a more complex solution than you
originally suspected. In this posting, I
will give some advice on completing this task and even present a useful
function that isn’t normally available.
DISCLAIMER
This posting and the accompanying code within are presented
in the hope that they may be useful, but WITHOUT ANY WARRANTY. AppVenture, Inc. is not liable for any
results of using this code, especially if you run it, without testing, on a
production system.
The Setup
Let’s describe the specifics of our situation before we get
started. Mail is accessed from an
Exchange server through Outlook 2003.
The Exchange server contains a public folder (called “ContactFolderName”)
that contains Contacts. A user may mark
certain contacts for synchronization; this is stored in the “Categories” field
as a list of comma-separated usernames, one for each interested user. Contacts also contain an ID, stored in the
field User4. Why User4? It’s unobtrusive
and unlikely to already be used—but if it is being used for something else,
pick another field.
Most users have only create, read and update permissions on
this folder – no delete. Administrators,
however, will also be using this program, so we must act as if the user does
have delete permission.
Our objective is to write a plugin that finds the marked
contacts and copies them to our local contacts folder, updating them if they
were already present. We must avoid
duplicates everywhere and must never delete from the public folder.
Our development environment is Visual Studio 2005, using
Visual Basic and the Visual Studio Tools for Office (VSTO).
Creating an Outlook Addin
This posting won’t address how to create an addin for
Outlook because there are plenty of articles that address just that. (See http://support.microsoft.com/?kbid=302901
). It assumes that you have already
created the infrastructure needed, i.e., there is a button that, when clicked,
calls btnLaunch_Click (below).
Finding the desired contacts
Before we can copy any contacts, we have to find the ones
we’re interested in, and before we can do that, we have to get their
folder. Fortunately, getting the folder
is easy, because we know right where it is: \\Public Folders\All Public
Folders\ContactFolderName . So, first we get the namespace, and then we create
the publicFolder variable as a MAPIFolder, and get the public contact folder
from the namespace (with a little error handling just in case).
Next, we have to look through the folder to find the items
we want. There are several ways of doing
this. We could loop through every item
in the folder, but that’s expensive and slow when there are thousands of
contacts in the folder. We could use a
Find, but find can’t perform ‘like’ searches, meaning we’d still have to
iterate through a bunch of items.
Instead, we’re going to get a little more complex, and
create an AdvancedSearch (see http://msdn2.microsoft.com/en-us/library/aa220071(office.11).aspx
). Getting the path is easy – just ask
the folder for it, and don’t forget the single quotes! – but determining the
field for the filter can be much harder because the schema is pretty large, and
not all the names in the schema correspond to ContactItem property names. Fortunately, there’s an easy way to determine
what to use. Start up Outlook and go to
any folder. Right-click on the
background of the folder and choose “Filter…”.
Go to the ‘Advanced’ tab and choose the field you want; in our case,
“All Contact Fields” -> “Categories”.
Then, choose your criterion (“contains”), fill in the value and hit ‘Add
to List’. Next, click on the “SQL” tab
and then check “Edit these criteria directly.”
There it is! Copy this straight
into the code and replace the value as above in the code, and pay attention to
the quotation marks. (You can close Outlook now).
Public Sub btnLaunch_Click(ByVal Ctrl As CommandBarButton, ByRef CancelDefault As Boolean) Handles btnLaunch.Click
Dim namespace As Microsoft.Office.Interop.Outlook.NameSpace
namespace = applicationObject.Session
Dim publicFolder As MAPIFolder = Nothing
Try
publicFolder = namespace.Folders("Public Folders").Folders("All Public Folders").Folders("ContactFolderName")
Catch ex As COMException
MessageBox.Show("Unable to find Contact folder", "Missing Folder", MessageBoxButtons.OK, MessageBoxIcon.Error)
Throw ex
End Try
Dim filter As String = """urn:schemas-microsoft-com:office:office#Keywords"" LIKE '%" + _
namespace.CurrentUser.Name + "%'"
Dim search As Search = applicationObject.AdvancedSearch("'" + publicFolder.FolderPath + "'", filter)
End Sub
Retrieving the Search Results
All well and good, you
say. But our previous function ended a
bit abruptly, and we haven’t accomplished our goal yet! Fear not, for now it is time to
ProcessSearchResults.
We ended our btnLaunch_Click function seemingly prematurely
for a very good reason: to give the AdvancedSearch time to complete. Because AdvancedSearches run in the
background, if we had tried to get the results right away, we might have missed
some, or even gotten none at all!
Fortunately, AdvancedSearch fires an event when it’s finished:
AdvancedSearchComplete, so all we have to do is have our ProcessSearchResults
function handle it, and our addin will proceed when it’s done.
NOTE: Other addins or even the user could run
AdvancedSearches that would cause ProcessSearchResults to execute, so you
should use a tag – which this code does not – to ensure you’re getting the
right search back. See the link on
AdvancedSearch above for more information.
We again get ahold of the namespace and then we ask for the
results of the search. Then, we find our destination folder (our local contacts
folder, in this case) and use ClearContactsFromFolder to delete the old contacts
from it. We ensure that it’s only the
public folder contacts by checking to see if the contact has a non-empty User4
field.
Private Sub ProcessSearchResults(ByVal SearchObject As Microsoft.Office.Interop.Outlook.Search) Handles applicationObject.AdvancedSearchComplete
namespace = applicationObject.Session
Dim searchResults As Results = SearchObject.Results
Dim contactsFolder As MAPIFolder = namespace.Folders("Mailbox - " + _namespace.CurrentUser.Name).Folders("Contacts")
ClearContactsFromFolder(contactsFolder)
CopyContactsToFolder(searchResults, contactsFolder)
While Marshal.ReleaseComObject(SearchObject) > 0
End While
SearchObject = Nothing
MessageBox.Show("Contacts Synchronized!")
End Sub
Public Sub ClearContactsFromFolder(ByRef folder As MAPIFolder)
Dim itemInFolder As Object = Nothing
For Each itemInFolder In folder.Items
Try
If Not itemInFolder Is Nothing Then
Dim itemclass As OlObjectClass
itemclass = CType(itemInFolder.class, OlObjectClass)
If itemclass = OlObjectClass.olContact Then
Dim newOlContactItem As ContactItem = CType(itemInFolder, ContactItem)
If Not newOlContactItem.User4 Is Nothing And Not newOlContactItem.User4 = String.Empty Then
newOlContactItem.Delete()
End If
End If
End If
Catch
End Try
Next
End Sub
Then, we copy the contacts to our destination folder using
CopyContactsToFolder (see below for code and discussion) and notify the user
that we’re done!
Copying contacts is a pain when accessing Outlook
programmatically. For some reason,
although ContactItem has a Copy function, it only copies to the same
folder. Now, if we had full permissions
on the public folder, this could work, because we could create a copy of the
ContactItem and then Move it to our desired folder. Unfortunately, a Move is a copy followed by a
delete, but we don’t have delete permissions on this folder in all cases!
Strangely, and don’t try this at home, a Move that fails due
to delete permissions results in a folder-to-folder copy! Why not use that? Two reasons: first, some users will have permission to delete, and we
don’t want to remove things from the public folder, and second, although we
could perform another copy-and-move if we detected that the user had delete
permissions, this is trouble just waiting for a network outage.
So, what is the
solution? Create a new ContactItem in a
folder the user controls (their default contacts folder), copy all the values
over, and move it to the destination folder (the same in our particular case,
but it doesn’t have to be). And that’s
what CopyContactsToFolder does: it takes in a results object, goes
through each one, creates a new instance, copies over all the fields, saves the
new instance and moves it to the desired folder. It’s a doozy, but it copies every ContactItem
field available (and that can be assigned to) in the ContactItem interface.
A brief note on why ‘item’ is an Object and not a
ContactItem: the search results that I dealt with when originally writing this
addin contained items that acted like ContactItems for all intents and
purposes, but couldn’t be cast into the ContactItem class. So, they’re just plain Objects in this code;
if you’re certain that all your results will be ContactItems, feel free to
change this (it will certainly get rid of a lot of warnings about late
binding!).
Copying Contacts
(Download source for entire function)
Public Sub CopyContactsToFolder(ByRef contacts As Results, ByRef folder As MAPIFolder)
Dim item As Object
For Each item In contacts
Try
Dim newItem As ContactItem = _
CType(applicationObject.CreateItem(OlItemType.olContactItem), ContactItem)
newItem.Account = item.Account
newItem.Anniversary = item.Anniversary
newItem.AssistantName = item.AssistantName
.
.
.
newItem.UserCertificate = item.UserCertificate
newItem.WebPage = item.WebPage
newItem.YomiCompanyName = item.YomiCompanyName
newItem.YomiFirstName = item.YomiFirstName
newItem.YomiLastName = item.YomiLastName
newItem.Save()
If Not folder Is _namespace.GetDefaultFolder(OlDefaultFolders.olFolderContacts) Then
newItem.Move(folder)
End If
Catch
End Try
Next
End Sub
ContactSync.vb (11.15 KB)