Wednesday 19 August 2009

The Managed Exchange EWS API (vb.net) - part 1

Ok so this is my first blog EVER (and its not even about Electronic Document Management - EDM - how bout that?), so constructive criticim is welcome in the comments but go easy on me - its my first time :-)

Want to start by saying ... Hurrah! We have the start of a a managed interface into the Exchange 2007/2010 server environments thanks to the efforts of the Exchange development team (See the announcement here - well done guys - keep up the good work)

So to celebrate this new API, and because I wanted to write my first blog, and I happen to have to write the code anyway, I decided to share my experiences with you in creating some code for this new interface..

Now ive only been using this thing a matter of hours so Im learning as I go and dont claim to have ALL of the answers, but wanted to put this out there to help people who are also starting out with it and find some people in similar situations doing different things that we might learn together..

This post will hopefully serve as a "getting started" guide and also give some example code (something there is a lack of outside the specialist MS forums at the moment).

Release Candidate launched (the day I posted this blog)...
Ive just updated this post to be compatible with the RC version of the code as it was literally launched a few mins ago (see announcement here) ...

So onwards ...

Audience
Im going to assume that those reading this are already very familiar with the Visual Studio environment and object orientated programming, using third party APIs and , and have some knowledge of exchange (probably more than me).

The Task
Fairly simple requirements. The task is to create a some code that will use an admin account to scan a list of user's mailboxes and permanently delete items in their "Deleted Items" folder that are older than a specified number of days (30).

Note: This code does not cover subfolders of the "Deleted Items" folder (I have got this working but dont have time to write it up right now)

This blog will focus on PURELY the Managed EWS API code to achieve this for a single user, you can wrap your own services/applications/loading from lists to your hearts content. I dont want to crowd the Managed EWS API code with standard .NET development stuff as it will get too complex to read.

Approach
So ive written something similar before to be used on Exchange 2003, the old project used WebDAV and logged onto peoples mailboxes through this mechanism, it worked a treat. However it appears that there are parts of WebDAV that have been removed in Exchange 2007 and in general its on its way to the big API graveyard in the sky (see the "Get your APIs right" section).

Im using VB.NET for this as it is part of the request from the client. If you need a hand converting it to C# either stick it in this magic thing or leave a comment here and Im sure someone (or I will) help you produce a C# version of the code.

Get your API's right
First things first, there are several API's available to interact with exchange from MAPI and CDO to WebDAV and webservices. There are details of all of these here...

List of APIs and their respective fates

The one we are using here is the BETA of the Managed EWS (Exchange Web Services) API and NOT a set of proxy classes generated from the Exchange web services.

The EWS generated proxy classes can look similar when searching for documentation on the internet however the Managed EWS API is the one we are using here.. It is true that these managed classes do use the exchange web services but simply adding a reference to an intellisensed library is far easier than generating proxy classes and working with them.


Step 1: Getting the DLLs and references sorted out
First step is to go here and download yourself a copy of the development library. Install this to somewhere on your machine and then jump into Visual Studio and create yourself a new Windows Application.

Note: Im using Visual Studio 2005 for this, however 2008 is fine as well.

  1. In solution explorer, right click the project and choose "Add Reference" from the menu
  2. In the add reference screen, browse to where you installed the API, by default this should be "C:\Program Files\Microsoft\Exchange\Web Services\1.0"
  3. Double click the "Microsoft.Exchange.WebServices.dll" file to add this as a reference.

Step 2: What you need to get this code working
You will need the following environment specific information to get this working on your own environment
  • Login details for an exchange administrator user (this is used to access each of the specified user's mailboxes)
  • URL to your exchange server web services - takes the format of http://servername/EWS/Exchange.ASMX

Step 3: The code
So now we have a WinForms project with a Form1 and a Reference to the Microsoft.Exchange.WebServices namespace.

  1. Right click on the project in Solutions manager again, and choose "Properties", set the "Application Type" dropdown to "Console App" so we can have a windows form but also see the output on screen as well (you can use Debug.Print() if you prefer, I use Console.WriteLine() as a personal preference. Close the properties screen when you are done.
  2. Go create a button on the form, you can leave it called "Button1", double click it to get our code block generated and start coding..

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles button1.Click

' -- our code will go here ---

End Sub



Ok so the first thing to do is create a connection to our exchange web services so first off, stick the imports at the top of your file,

Imports Microsoft.Exchange.WebServices
Imports Microsoft.Exchang
e.WebServices.Autodiscover
Imports Microsoft.Exchange.WebServices.Data


Next we need (inside our Button1_Click sub) to connect to our exchange server via the web service URL and also specify the version of exchange we are talking to (Note: Only Exchange 2007 SP1 and 2010 are supported).

So First we define a the connector object...

Dim exch As ExchangeService = New ExchangeService(ExchangeVersion.Exchange2007_SP1)

Next we need to specify which user to connect to exchange with. Now in the case of our task, we need to connect as a user who has admin rights over the exchange environment so we can dip in and out of everyones mailboxes.

For now (we will talk about AutoDiscover() some other time), we are going to hard code this URL to our exchange server in the format of "https://SERVERNAME/EWS/Exchange.asmx"

So throw in the following code...

exch.Url = "https://SERVERNAME/EWS/Exchange.asmx"
exch.UseDefaultCredentials = False
exch.Credentials = New System.Net.NetworkCredential("ADMIN_USER","ADMIN_PWD", "ADMIN_DOMAIN")

Note: I simply used the normal login credentials of our admin user here rather than FQDN of the user and it seemed to work fine.

Next we want to be able to impersonate a user so we can go and snoop their email (and other) folders and start to find and rid ourselves of these old emails.

There are several ways you can resolve to a user account when using the constructor of ImpersonateUserId, in this code I am using the PrincipalName which refers to the users full account name on the active directory (see the screenshot below from AD Users And Computers.


exch.ImpersonateUserId = New ImpersonateUserId(ConnectingIdType.PrincipalName, "edmguy@ad.mydomain.com")

Ok so now we have set up the conncetor to log on as our admin user and impersonate our user, now we need to connect to a folder to find some items. So lets take our first folder "DeletedItems" and see if we cant return how many our user has in that folder.

So to do this we need to do a search on items within a specific folder, and the EWS managed API allows us to do this very easily.

First we need to set up an ItemView object which determines parameters of our results (this objec holds properties and collections regarding the search results - things like filters, page sizes, fields to return, etc) , too many to explore right now, but something we can come back to in the future, for now lets focus on getting some results out of exchange...

Dim iv As ItemView = New ItemView(999) ' return first 999 pages

The only property we are going to set on our ItemView object is the traversal, there are three options to this setting Associated, SoftDeleted and Shallow. We are going to use Shallow in this code to return Non-Deleted items in this folder...

iv.Traversal = ItemTraversal.Shallow


Ok so we now have to make a call to the web service to search for items in this folder. We are given a nice simple generic collection of Item returned form the web service in the form of the FindItemResults object. So to perform a search for items, we need to initialise one of these object like so...

(Note: Ive split the following line so its easier to read on the blog, you could simply put the call to the service on the same line as the decleration, or bypass the decleration all together and have a for..each that works directly off of the call to the service)

Dim deletedItems As FindItemsResults(Of Item) = Nothing

Next we need to populate this object by making a call to our exchange service using the FindItems method.

FindItems has 4 overloaded constructors, two of which allows you to target a particular folder with or without an object to group the results and the other two allow you to target WellKnownFolders (such as Inbox, DeletedItems, SentItems, etc) again with or without a folder for you to group results with.

For the purposes of the blog and getting things working, we are simply going to go for the one that targets a WellKnownFolder without any grouping of results

deletedItems = exch.FindItems(WellKnownFolderName.DeletedItems, iv)


So this (when executed) should have populated the deletedItems object with a list of Item objects which represent emails from our impersonated users "deleted folder" within exchange.

Just so we can satisfy ourselves that this works, we can put together a simple for..each loop to go thru the items and write out to the Console or Debug output screens and run the code to make sure we have the right results...

For Each i As Item In deletedItems
Console.WriteLine(i.Subject)
Next

Assuming everything is ok, we now need to find out which items in this folder are older than (lets say) 30 days. The quick and dirty way to do this would be to put a check on the sent or received date inside the loop but there is a better way... By passing SearchFilters into our search method.

So head back to where we declare ourItemView object and now we want to add a couple of simple search filters to return any item sent or recieved before 30 days ago. To do this we need to use the SearchFilter.SearchFilterCollection (as we have more than one criteria and we are linking our criteria with the OR operator) and so we first need to delare this like so...

Dim filters As SearchFilter.SearchFilterCollection = New SearchFilter.SearchFilterCollection(LogicalOperator.Or)


Next we need to say, add a criteria to only return Items that have a received date before 30 days ago, to do this we create a new SearchFilter object and add it to our Filters collection. The EWS Managed API provides an enumerator for the ItemSchema to make it easy to access the properties of an item. Our criteria would be defined and added to our Filters collection like so...

filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeReceived, DateTime.Today.AddDays(-30)))

filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeSent, DateTime.Today.AddDays(-30)))

So now the code block for defining the item view should look something like this...

Dim iv As ItemView = New ItemView(999)
iv.Traversal = ItemTraversal.Shallow

Dim filters As SearchFilter.SearchFilterCollection = New SearchFilter.SearchFilterCollection(LogicalOperator.Or)
filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeReceived, DateTime.Today.AddDays(-30)))

filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeSent, DateTime.Today.AddDays(-30)))


Now we need to change the actual search line to pass in our filter, like so...

Dim deletedItems As FindItemsResults(Of Item) = _exch.FindItems(WellKnownFolderName.DeletedItems, filters, iv)

And that *should* be it, we now have something that connects to exchange, attached to a users mailbox (based on a string that you can change) and performs a search on their mailbox for items sent or received over 30 days ago. Now to delete them, under the Console.WriteLine() statement simply add the code to delete the item...

i.Delete(DeleteMode.HardDelete) ' -- warning! this gets rid of the item permanently

Add a msgbox at the end so you know when its finished and that brings us nicely to the end of my first blog. Hope its helpful to someone out there. There is quite a bit more to explore on this API and I have another upcoming challenge so that might be part 2.

Please feel free to comment, question, leave improvements to code, etc here. Like I said at the top, I dont have all the answers but between us Im sure we can work them out.






Full Code Section - for those who dont like to read too much :-)

Ive left the delete part of the code remarked out as this is dangerous and you may want to test things out before running it on your exchange environment.


Imports Microsoft.Exchange.WebServices
Imports Microsoft.Exchange.WebServices.Autodiscover
Imports Microsoft.Exchange.WebServices.Data

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles button1.Click

' -- our code will go here ---

Dim exch As ExchangeService = New ExchangeService(ExchangeVersion.Exchange2007_SP1)

exch.Url = "https://SERVERNAME/EWS/Exchange.asmx" '
exch.UseDefaultCredentials = False
exch.Credentials = New System.Net.NetworkCredential("ADMIN_USERNAME","ADMIN_PWD", "ADMIN_DOMAIN") '
exch.ImpersonateUserId = New ImpersonateUserId(ConnectingIdType.PrincipalName, "edmguy@ad.mydomain.com") '


Dim iv As ItemView = New ItemView(999) '
iv.Traversal = ItemTraversal.Shallow '

Dim filters As SearchFilter.SearchFilterCollection = New SearchFilter.SearchFilterCollection(LogicalOperator.Or)

filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeReceived, DateTime.Today.AddDays(-30)))

filters.Add(New SearchFilter.IsLessThan(ItemSchema.DateTimeSent, DateTime.Today.AddDays(-30)))

Dim deletedItems As FindItemsResults(Of Item) = Nothing
deletedItems = exch.FindItems(WellKnownFolderName.DeletedItems, filters, iv)

' -- enum items to be deleted --
For Each i As Item In deletedItems
Console.WriteLine(i.Subject)
' i.Delete(DeleteMode.HardDelete)
Next

Msgbox("Done!")

End Sub



Troubleshooting
I had quite a bit of fun simply getting the code to log on and trust the server certificate and also got a number of "403 unauthorised" errors when I was coding this for the first time

Getting Exception : "This property was requested but was not returned by the server."
I got this error when I ran the code through my deleted items and I had deleted a draft item that had no send or receive date (when using the "how not to do it" code). Change the ItemView object to do the work for you so the array of Item objects being returned have all the values you need.

Getting Exception : "could not establish trust relationship for the ssl tls secure channel with authority"?:
When I was first trying to get this working internally, i kept getting the exception "could not establish trust relationship for the ssl tls secure channel with authority". This turned out to be a problem with the certificates on our exchange server. The trusted authority certificate was for the URL "remotemail.company.com" and I was accessing the internal exchange server name which was "exchsvr01".

To solve this, I placed an entry in my HOSTS file ("c:\windows\system32\drivers\etc\hosts") with the trusted certificate name pointing at the internal IP address and it got round my problems for now until the networks guys can sort out the DNS for me internally.

Info from wikipedia on hosts files is here