Using .NET and WebDAV to access an Exchange server

October 26, 2006

If your company runs on Exchange (and who doesn’t), there will come a day when you’ll need to gather or modify data from an external application or a web page you’re building. There are many tools for doing this, from expensive third-party libraries to dead technologies buried in MSDN. The method I found best was a mix of C#, .NET and WebDAV, all of which are modern, flexible, and recommended by Microsoft. WebDAV (basically an HTTP request that can write as well as read) is used in many applications, including Microsoft’s own Entourage. It’s reliable and easy to use, but there are plenty of potential problems if you’re starting from scratch. Here’s a short tutorial that should give you all the basics for building Exchange support into your own application.

Getting Started

Because WebDAV is built on standard HTTP requests, the .NET library classes can handle many of the gritty details. We need to start by authenticating with a set of login credentials to get an authorization cookie, request the data we need, and extract it into a format we understand. To keep things simple, I’m only going to create one class, WebDAVRequest. I’m going to stick a few variables in here, to define things like the username and server address. Remember to URL encode the path if it contains any non-alphanumeric characters.


using System;
using System.IO;
using System.Xml;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;
using System.Text.RegularExpressions;

namespace DowntownSoftwareHouse.ExchangeExample
{
    public class WebDAVRequest
    {
        private static string server = "https://mail.mycompany.com";
        private static string path = "/public/Calendars/Board%20Room";
        private static string username = "user"
        private static string password = "password";
        private CookieContainer authCookies = null;
    }
}

Authentication

We’re going to be using forms based authentication in this example. The code you need to write does pretty much the same thing as when you log into your OWA server using a web browser; the local client checks the server’s certificate, attempts to authenticate using your credentials, and stores a cookie for later requests. Put these methods into the class we just wrote.


/// 
/// Add code here to check the server certificate, if you think it's necessary. Note that this
/// will only be called once when the application is first started.
/// 

protected bool CheckCert( Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors )
{
    return true;
}

/// 
/// Default WebDAVRequest constructor. Set the certificate callback here.
/// 

public WebDAVRequest()
{
    ServicePointManager.ServerCertificateValidationCallback += CheckCert;
}

The CheckCert event handler is called once when you first connect to the Exchange server. You’ll see that I’m cheating here; this code doesn’t actually do anything except allow us to move on to the next step. Whether or not you verify the certificate’s authenticity depends on the security of your network, and I’ll leave that decision up to you. For now, let’s move on.


/// 
/// Authenticate against the Exchange server and store the authorization cookie so we can use
/// it for future WebDAV requests.
/// 

public void Authenticate()
{
    string authURI = server + "/exchweb/bin/auth/owaauth.dll";

    // Create the web request body:

    string body = string.Format( "destination={0}&username={1}&password={2}", server + path, username, password );
    byte[] bytes = Encoding.UTF8.GetBytes( body );

    // Create the web request:

    HttpWebRequest request = (HttpWebRequest)System.Net.WebRequest.Create( authURI );
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";
    request.CookieContainer = new CookieContainer();
    request.ContentLength = bytes.Length;

    // Create the web request content stream:

    using ( Stream stream = request.GetRequestStream() )
    {
        stream.Write( bytes, 0, bytes.Length );
        stream.Close();
    }

    // Get the response & store the authentication cookies:

    HttpWebResponse response = (HttpWebResponse)request.GetResponse();

    if ( response.Cookies.Count < 2 )
        throw new AuthenticationException( "Login failed. Is the login / password correct?" );

    cookies = new CookieContainer();
    foreach ( Cookie myCookie in response.Cookies )
    {
        cookies.Add( myCookie );
    }

    response.Close();
}

Things are getting a bit trickier now. This might be a little unfamiliar if you haven’t worked with HTTP data streams in the past, but .NET encapsulates most of it. All we’re doing is sending an HTTP request (which contains our login credentials, as well as the URL to the resource we’re requesting access to) to the Exchange OWA server’s authorization .dll, and reading back the response. If everything goes okay, we’ll get a few cookies which will be used later to access the resource we need. Note that we have to move these from a CookieCollection to a CookieContainer; for reasons I don’t know .NET doesn’t let you intermix the containers for requests and responses.

Don’t forget to close the response stream; you’ll get errors the next time you make a request if you don’t.

Gathering Data

At this point, we’re finally ready for whatever it is we need to do with Exchange. In this example, I’m going to poke around in the public folders and gather information for the day’s calendar appointments. I’ll also use a regular expression to find the organizer’s full name; the full string contains their name and email address, which might not be suitable for what you’re trying to display.


/// 
/// Find today's appointments in the public folder calendar and print the results.
/// 

public void RunQuery()
{
    string uri = server + path;
    HttpWebRequest request;
    WebResponse response;
    byte[] bytes;

    string start = DateTime.Today.ToString( "yyyy/MM/dd" );
    string end = DateTime.Today.ToString( "yyyy/MM/dd" );

    // Note that deep traversals don't work on public folders. In other words, if you
    // need to dig deeper you'll need to split your query into multiple requests.

    string format =
        @"
            <g:searchrequest xmlns:g=""DAV:"">
                
                    SELECT
                        ""urn:schemas:calendar:dtstart"", ""urn:schemas:calendar:dtend"",
                        ""urn:schemas:httpmail:subject"", ""urn:schemas:calendar:organizer"",
                        ""DAV:parentname""
                    FROM
                        Scope('SHALLOW TRAVERSAL OF ""{0}""')
                    WHERE
                        NOT ""urn:schemas:calendar:instancetype"" = 1
                        AND ""DAV:contentclass"" = 'urn:content-classes:appointment'
                        AND ""urn:schemas:calendar:dtstart"" > '{1}'
                        AND ""urn:schemas:calendar:dtend"" < '{2}'
                    
            </g:searchrequest>";

    bytes = Encoding.UTF8.GetBytes( String.Format( format, uri, start, end ) );

    // Use the authorization cookies we stored in the authentication method.

    request = (HttpWebRequest)HttpWebRequest.Create( uri );
    request.CookieContainer = authCookies;
    request.Method = "SEARCH";
    request.ContentLength = bytes.Length;
    request.ContentType = "text/xml";

    using ( Stream requestStream = request.GetRequestStream() )
    {
        requestStream.Write( bytes, 0, bytes.Length );
        requestStream.Close();
    }

    response = (HttpWebResponse)request.GetResponse();

    using ( Stream responseStream = response.GetResponseStream() )
    {
        // Parse the XML response to find the data we need.

        XmlDocument document = new XmlDocument();
        document.Load( responseStream );

        XmlNodeList subjectNodes = document.GetElementsByTagName( "e:subject" );
        XmlNodeList locationNodes = document.GetElementsByTagName( "a:parentname" );
        XmlNodeList startTimeNodes = document.GetElementsByTagName( "d:dtstart" );
        XmlNodeList endTimeNodes = document.GetElementsByTagName( "d:dtend" );
        XmlNodeList organizerNodes = document.GetElementsByTagName( "d:organizer" );

        for ( int index = 0; index < subjectNodes.Count; index++ )
        {
            string subject = subjectNodes[index].InnerText;
            string organizer = organizerNodes[index].InnerText;
            string location = ParentName( locationNodes[index].InnerText );
            DateTime startTime = DateTime.Parse( startTimeNodes[index].InnerText );
            DateTime endTime = DateTime.Parse( endTimeNodes[index].InnerText );

            // Use a regex to get just the user's first and last names. Note that
            // some appointments may not have a valid user name.

            string pattern = @"""(?.*?)""";
            Regex regex = new Regex( pattern, RegexOptions.None );
            Match matchedText = regex.Match( organizer );

            if ( matchedText.Success && matchedText.Groups["name"] != null )
                organizer = matchedText.Groups["name"].Value;

            // Print the results to the console.

            Console.WriteLine( "{0} - {1}: {2}", startTime.ToShortTimeString(), endTime.ToShortTimeString(), subject );
            Console.WriteLine( "{0} ({1})", location, organizer );
            Console.WriteLine();
        }

    }

    response.Close();
}
</code></pre>

Just as before, we're making an HTTP request and reading back the response. The body of the request is XML markup that contains the Exchange data store query. The query itself is similar (both in structure and power) to SQL, so you should feel right at home if you've worked with databases before. It's possible to use a deep traversal or specify multiple folders in the FROM clause, as long as you're not looking in the public folders. If you are, you'll need to make multiple WebDAV requests to get the data you need.

The response is also XML, and since .NET has excellent XML support, we don't have to do much work to parse it. As I mentioned before, I used a regular expression to change one of the attributes, but that part is really optional. The format of both the request query and response is complex (to say the least), but everything you need should be in MSDN. I've included a few links at the end of the article to get you started.

Finishing Up

All that's left now is to run the query! I'll also do a little error handling here, and catch the exceptions I'm likely to run into.


static void Main( string[] args )
{
    try
    {
        WebDAVRequest request = new WebDAVRequest();
        request.Authenticate();
        request.RunQuery();
    }
    catch ( SecurityException e )
    {
        Console.WriteLine( "Security Exception" );
        Console.WriteLine( "   Msg: " + e.Message );
        Console.WriteLine( "   Note: The application may not be trusted if run from a network share." );
    }
    catch ( AuthenticationException e )
    {
        Console.WriteLine( "Authentication Exception, are you using a valid login?" );
        Console.WriteLine( "   Msg: " + e.Message );
        Console.WriteLine( "   Note: You must use a valid login / password for authentication." );
    }
    catch ( WebException e )
    {
        Console.WriteLine( "Web Exception" );
        Console.WriteLine( "   Status: " + e.Status );
        Console.WriteLine( "   Reponse: " + e.Response );
        Console.WriteLine( "   Msg: " + e.Message );
    }
    catch ( Exception e )
    {
        Console.WriteLine( "Unknown Exception" );
        Console.WriteLine( "   Msg: " + e.Message );
    }
}
How you integrate this in your own application is up to you, and ultimately depends what it is you're trying to accomplish. One of the things I like about .NET is how flexible and reusable it often is. In my case, I put the Exchange handling code and related classes in its own class library, which is imported into the ASP .NET webapp that displays the data to the user. When it's time to update the library, or create a different ASP .NET project that uses the code, all I have to do is copy a single .dll into the project. Here are a few URLs you might need as you go. Searching Calendar Folders with WebDAV Exchange Store Key Tasks Exchange Programming Technologies

Marc Charbonneau is a mobile software engineer in Portland, OR. Want to reply to this article? Get in touch on Twitter @mbcharbonneau.