Earlier I mentioned a Java Exchange Connector I had seen, and in that post I also said that I had some code that connected from Java to Exchange Server via WebDAV. Some people have asked for the code, so here it is.

I use a set of Apache libraries to make this possible: Apache Commons HttpClient for the Java SSL client capability, and Apache Jakarta Slide for the WebDAV piece. As you may have heard, Slide was discontinued as a project a while ago. So maybe you are thinking, Slide? What good is that code if it depends on an abandoned Apache library?. A reasonable point. Honestly though, the Slide stuff is really not that critical for this scenario. Basically, WebDAV is a standard for formatting queries that get sent over HTTP. But in my case, I am just cons-ing up strings that contain the queries. It would be pretty easy to factor out the WebDAV/Slide stuff, I think. I didn't bother to do it, only because I wrote the code a long time ago, well before Slide was discontinued, and I didn't really feel like investing the time to re-factor it now.

The httpclient and slide jars also drag in the commons-logging and codec jars from Apache.

I have the full working proof-of-concept code attached to this post as a zipfile. It is a JSP-based app that runs in Tomcat or Jetty or your favorite servlet container. Requires JDK 1.5 to build it. You can have a look and try it out yourself. Here's the code I use to send out a query to Exchange.

  public org.w3c.dom.Document search(String urlString, String request, int depth, String range) 
    throws Exception
  {
    Document doc= null;

    SearchMethod method = new SearchMethod(urlString, request); 
    try {
      //method.setRequestHeader("Content-type", "text/xml");
      method.setRequestHeader("depth", ""+depth) ;
      method.setRequestHeader("Translate", "f");

      // must set Content-Length explicitly.  Why doesn't the SearchMethod do this?  Who knows. . .
      method.setRequestHeader("Content-Length", String.valueOf(request.length()));

      if ((range!=null) && (range !=""))
          method.setRequestHeader("Range", range);

      int rc = httpclient.executeMethod(method);

      doc = method.getResponseDocument();
    }

    finally {
      // release any connection resources used by the method
      method.releaseConnection();
    }            

    return doc;
  }

 

Ok, that's just some boilerplate WebDAV stuff. Get a request, set the HTTP headers, send out the request. The magic really is in the request itself. This is what a WebDAV request looks like on the wire:

SEARCH /exchange/dinoch/Inbox HTTP/1.1 
depth: 1 
Translate: f 
Content-Length: 370
Range: rows =0-3
Content-Type: text/xml; charset=utf-8
User-Agent: Jakarta Commons-HttpClient/3.1
Host: mail.microsoft.com
Cookie: $Version=0; sessionid=40e73022-93d3-4739-99ff-bf60fd60fcee; $Path =/
Cookie: $Version=0; cadata=1 gGfmglWDAmcLGmstqx3kSpuecb1BVBmjFwiD0IIem2hm6IsfCLr7DSo63ncgdM7xNi3E5A==; $Path= /

<searchrequest xmlns='DAV:' >
  <sql> 
    SELECT "DAV:id", "DAV:href" , "urn:schemas:httpmail:subject", "urn:schemas: httpmail:from", "urn:schemas:httpmail:datereceived" 
    FROM SCOPE('shallow traversal of "https://mail.microsoft.com/exchange/dinoch/Inbox"') 
    WHERE "DAV:ishidden"=False AND "DAV:isfolder"=False 
  </sql>
</searchrequest>

 

In that request, I'm searching on my Exchange Inbox. I can also search on any folder: Calendar, Contacts, Notes, Tasks, any mail folder, and so on. The schema are different for different item types, so ya gotta be careful there. Anyway, in the above request, I search on the first 4 rows, and I ask for the fields: subject, id, href, from, and datereceived.

In the proof of concept, I create these queries using a file-based template, one for each type of query. I have a template for the inbox query, another template for the query of the tasks folder, and so on. This is the template for the Tasks query, for example:

<searchrequest  xmlns='DAV:'>
  <sql>
    SELECT 
       "DAV:href", 
       "DAV:displayname", 
       "DAV:getlastmodified",
       "urn:schemas:httpmail:subject",
       "urn:schemas:httpmail:textdescription",
       "http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-C000-000000000046}/0x00008517" as DueDate2,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8101" AS Status,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8102" AS PercentComplete,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8104" AS StartDate,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8105" AS DueDate,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x810f" AS DateCompleted,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x811c" AS IsComplete,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8113" AS State,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8110" AS ActualEffort,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8111" AS EstimatedEffort,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8518" AS Mode,
       "http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x811f" AS Owner

    FROM SCOPE('shallow traversal of  "##FOLDERPATH##"')
    WHERE "DAV:ishidden"=False AND "DAV:isfolder"=False  
    ORDER BY IsComplete, DueDate ASC 

 </sql>
</searchrequest>

 

One of the trickiest areas for me was just figuring out the MAPI schema for all the various fields that might be present in a given document type (like task, message, meeting request, contact, note, and so on) that can be stored by Exchange. The schema are not intuitive, nor did I find the documentation to be easily accessible. As you can see, there are some magic incantations above for the task document type.

Just so everyone is clear: The schema used is the same, regardless of the type of client. I am writing a Java app here. But you could use these same queries from a .NET app, a VBScript app, or a PHP app, or Ruby or whatever. The Exchange Server responds to the on-the-wire protocol, and doesn't care about the client you use.

Another key part is logging into the Exchange server. This is the method I use to login.

  public void login (String destUrl, String username, String password) 
    throws Exception
  {
    Username= username;

    String[] urlParts=destUrl.split("/");
    String server= urlParts[2]; 
    String authDllPath= "/exchweb/bin/auth/owaauth.dll";
    String AuthUrl= Protocol + "://" + server + authDllPath;

    //System.setProperty("javax.net.debug", "all");  // too much information
    //System.setProperty("javax.net.debug", "ssl");

    // the class ionic.ssl.SSLSocketFactoryImpl must be available for the classloader,
    // eg, on \lib\ext
    java.security.Security.setProperty("ssl.SocketFactory.provider",
                                       "ionic.ssl.SSLSocketFactoryImpl");

    if (!server.equals(Servername)) 
      throw new Exception ("Cross-site redirection attempt.");

    PostMethod method = new PostMethod(AuthUrl);

    try {

      //System.out.println("DavConnection: Logging in with u/p: " + username + " | " + password); 

      method.setFollowRedirects(false); // false == default

      NameValuePair[] data = {
        new NameValuePair("destination", destUrl),
        new NameValuePair("username", username),
        new NameValuePair("password", password)
      };
      method.setRequestBody(data);

      int rc = httpclient.executeMethod(method);
      int stat= method.getStatusLine().getStatusCode(); 
        
      if (stat== 302) {  // 302 = Moved Temporarily (this is success)

        // The response form Exch2003 says "Moved Temporarily" but 
        // if we are only logging in, we don't need to follow this link. 
        // The apache commons httpclient runtime will log an INFO
        // saying "302 received but followRedirects==false.  This is OK. 

        // String redirectLocation= null; 
        // Header locationHeader = method.getResponseHeader("location");
        // if (locationHeader != null) 
        // redirectLocation = locationHeader.getValue();
        // System.out.println("Redirect to: " + redirectLocation);

      }
      else 
        throw new Exception("Unexpected HTTP status code during login (" + stat +")");
    } 
    finally {
      // release any connection resources used by the method
      method.releaseConnection();
    }            
  }

 

This login method is exposed on a DavConnection object, the class/type that wraps all the interaction with Exchange server. After login, of course, I set the DavConnection into the http session. It gets returned as a cookie to the browser. Thereafter when the browser connects and presents its cookie, we have te active Exchange connection. We only login once.

At this point I want to talk about cookies. My favorite kind are oatmeal cookies, which I love to make. When I make 'em, I tend to eat them, all of them. So I don't make 'em that often. My second-favorite kind of cookies are HTTP Cookies. There are two sets of HTTP cookies in this application scenario. One set of cookies links the browser to the JSP app. Another set of cookies links the JSP app to Exchange Server.

The second set, the cookies that Exchange sends back to the client that is authenticating (in this case our JSP app) must be retained and presented back to the server on subsequent queries. Nicely for us, the Apache HttpClient library does that automagically for us. The DavConnection class embeds an HttpClient instance as a member; this HttpClient instance retains the cookies that keep the conversation with Exchange alive. mmm-kay?

Let's see what else? I guess you will want to have a look at a sample response from an Exchange WebDAV query. This is one response I got.

<a:multistatus xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" xmlns:d="urn:schemas:httpmail:" xmlns:c="xml:" xmlns:a="DAV:">
  <a:contentrange>0-9</a:contentrange>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/RE:%20Developing%20and%20debugging%20without%20admin%20rights.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47Tx0AAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/RE:%20Developing%20and%20debugging%20without%20admin%20rights.EML</a:href>
        <d:subject>RE: Developing and debugging without admin rights</d:subject>
        <d:from>"Alun Jones" &lt;alunj@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T21:26:24.409Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/MarieHu%20team%20meeting:%20FY06%20plans%20%26%20budget.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxGAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/MarieHu%20team%20meeting:%20FY06%20plans%20%26%20budget.EML</a:href>
        <d:subject>MarieHu team meeting: FY06 plans &amp; budget</d:subject>
        <d:from>"Marie Huwe" &lt;mariehu@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T21:23:44.107Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Repeater%20control%20%3CItemTemplate%3E%20question.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxMAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Repeater%20control%20%3CItemTemplate%3E%20question.EML</a:href>
        <d:subject>Repeater control &lt;ItemTemplate&gt; question</d:subject>
        <d:from>"Mike Burdick" &lt;mikebu@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T21:23:06.000Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Work%20on%20the%20update%20to%20the%20Business%20Section%20of%20the%20RTB%20Slide%20Deck.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxZAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Work%20on%20the%20update%20to%20the%20Business%20Section%20of%20the%20RTB%20Slide%20Deck.EML</a:href>
        <d:subject>Work on the update to the Business Section of the RTB Slide Deck</d:subject>
        <d:from>"Paul Barcoe-Walsh" &lt;paulbwa@exchange.microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T20:52:48.000Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/RE:%20demo%20script%20updates%20-%2060min:%20403%20on%20notify%20ws.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxSAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/RE:%20demo%20script%20updates%20-%2060min:%20403%20on%20notify%20ws.EML</a:href>
        <d:subject>RE: demo script updates - 60min: 403 on notify ws</d:subject>
        <d:from>"Jonathan Moons" &lt;jmoons@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T20:46:05.000Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/ACTION:%20New%20launch%20BOM%20in%20effect.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxQAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/ACTION:%20New%20launch%20BOM%20in%20effect.EML</a:href>
        <d:subject>ACTION: New launch BOM in effect</d:subject>
        <d:from>"Bill Dunlap" &lt;bdunlap@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T20:43:09.000Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/You%20available%20to%20talk%20today_x003F_.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxRAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/You%20available%20to%20talk%20today_x003F_.EML</a:href>
        <d:subject>You available to talk today?</d:subject>
        <d:from>"Terry Leeper" &lt;tleeper@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T20:07:57.694Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/MarieHu%20team%20meeting:%20Overview%20of%20the%20Connected%20Systems%20Generico%20application.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxYAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/MarieHu%20team%20meeting:%20Overview%20of%20the%20Connected%20Systems%20Generico%20application.EML</a:href>
        <d:subject>MarieHu team meeting: Overview of the Connected Systems Generico application</d:subject>
        <d:from>"Marie Huwe" &lt;mariehu@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T20:02:45.693Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Blowfish%20implementations%20for%20.NET.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxKAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Blowfish%20implementations%20for%20.NET.EML</a:href>
        <d:subject>Blowfish implementations for .NET</d:subject>
        <d:from>"Kevin Hammond" &lt;kevinha@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T19:48:00.000Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
  <a:response>
    <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Out%20of%20Office%20AutoReply:%20SF%20and%20Interop.EML</a:href>
    <a:propstat>
      <a:status>HTTP/1.1 200 OK</a:status>
      <a:prop>
        <a:id>ARkAAAACUCbqAQAAQL47TxBAAAAA</a:id>
        <a:href>https://mail.microsoft.com/Exchange/dinoch/Inbox/Out%20of%20Office%20AutoReply:%20SF%20and%20Interop.EML</a:href>
        <d:subject>Out of Office AutoReply: SF and Interop</d:subject>
        <d:from>"Mike Wons" &lt;mikewons@microsoft.com&gt;</d:from>
        <d:datereceived b:dt="dateTime.tz">2005-04-11T19:35:23.673Z</d:datereceived>
      </a:prop>
    </a:propstat>
  </a:response>
</a:multistatus>

 

Just standard XML. I use an XSL sheet to transform that into some readable HTML. In the interest of completeness, here it is:

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0"
    xmlns:a="DAV:"
    xmlns:m="urn:schemas:httpmail:" 
    xmlns:c="urn:schemas:contacts:" 
    xmlns:java="java" 
  >

<!-- Contacts.xsl                                                         -->
<!--                                                                      -->
<!-- This sheet is used to Transform XML obtained from an Exchange        -->
<!-- webdav query into html.                                              -->
<!--                                                                      -->
<!-- Tue, 12 Apr 2005  15:11                                              -->


<xsl:param name="PageNumber"/>
<xsl:param name="UserName"/>
<xsl:param name="FolderName"/>
<xsl:param name="PageName"/>
<xsl:param name="CurrentTimeFormatted"/>


<xsl:output 
    method="html" 
    indent="yes" />

  <xsl:template match="a:multistatus">
    <xsl:variable name="FolderDecoded"
              select="java:net.URLDecoder.decode($FolderName, 'UTF-8')" >
    </xsl:variable>
    <xsl:choose>
      <xsl:when test="number($PageNumber) > 1">
      <a>
          <xsl:attribute name="href"><xsl:value-of select="$PageName"/>?action=itemizeFolder&amp;ref=<xsl:value-of select="$FolderName"/>&amp;page=<xsl:value-of select="number($PageNumber)-1" /></xsl:attribute>
            &lt;&lt;prev</a>
      </xsl:when>
      <xsl:otherwise>
      &#160; &#160; &#160; &#160; &#160;
      </xsl:otherwise>
    </xsl:choose>

    &#160;
    <a>
          <xsl:attribute name="href"><xsl:value-of select="$PageName"/>?action=itemizeFolder&amp;ref=<xsl:value-of select="$FolderName"/>&amp;page=<xsl:value-of select="number($PageNumber)+1" /></xsl:attribute>
           next&gt;&gt;
    </a>
    
    <h2>Folder:<xsl:value-of select="$FolderDecoded"/></h2>
    <h3>User: <xsl:value-of select="$UserName"/><br/>
    as of: <xsl:value-of select="$CurrentTimeFormatted"/></h3>
    <table border='1'> 
    <xsl:apply-templates select="*" />
    </table> 
  </xsl:template>

  <xsl:template match="a:contentrange" />

  <xsl:template match="a:response">

    <tr>
     <xsl:choose>

      <!-- ============================================================ -->
      <xsl:when test="$FolderName = 'Inbox' or $FolderName = 'Sent Items' or $FolderName = 'Deleted Items'  or $FolderName = 'Junk E-mail'  " >
      <td>
        <a>
            <xsl:attribute name="href"><xsl:value-of select="$PageName"/>?action=get&amp;item=<xsl:value-of select="a:href" /></xsl:attribute>
              <xsl:value-of select="a:propstat/a:prop/m:subject" />
        </a> 
        </td>
        <td style="font-size:8pt;">
              <xsl:value-of select="translate(a:propstat/a:prop/m:from, '\\', '')" />
        </td>
        <td>
           <xsl:value-of select="substring(a:propstat/a:prop/m:datereceived,0,11)" />&#160;<xsl:value-of select="substring(a:propstat/a:prop/m:datereceived,12,8)" />
        </td>

      </xsl:when>

      <!-- ============================================================ -->
      <xsl:when test="$FolderName = 'Contacts'">
        <td>
        <a>
            <xsl:attribute name="href"><xsl:value-of select="$PageName"/>?action=get&amp;item=<xsl:value-of select="a:href" /></xsl:attribute>
             <b> <xsl:value-of select="a:propstat/a:prop/c:fileas" /></b>
        </a> 
        </td>
        <td style="font-size:8pt;">
              <xsl:value-of select="a:propstat/a:prop/c:telephoneNumber" />
        </td>
      </xsl:when>

      <!-- ============================================================ -->
      <xsl:otherwise>
      <!-- xsl:when test="$FolderName = 'Notes'"  -->
        <xsl:variable name="ItemName"> 
        <xsl:choose>
          <xsl:when test="substring(a:propstat/a:prop/a:displayname,string-length(a:propstat/a:prop/a:displayname)-3) = '.EML'">
            <xsl:value-of select="substring-before(a:propstat/a:prop/a:displayname,'.EML')" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="a:propstat/a:prop/a:displayname" />
          </xsl:otherwise>
        </xsl:choose>
        </xsl:variable>

        <td>
        <a>
            <xsl:attribute name="href"><xsl:value-of select="$PageName"/>?action=get&amp;item=<xsl:value-of select="a:href" /></xsl:attribute>
             <b> <xsl:value-of select="$ItemName" /></b>
        </a> 
        </td>
        <td style="font-size:8pt;">
              <xsl:value-of select="a:propstat/a:prop/a:getlastmodified" />
        </td>
      </xsl:otherwise>
     </xsl:choose>
    </tr>
  </xsl:template>
</xsl:stylesheet>

 

Of course, in the more general case, you're not going to just be displaying the result you got from querying exchange. Instead you will be extracting data and mashing it up with something else. In that case you will want to do XPath or DOM walky stuff on the XML doc. And you know how to do that, right?

All the code is available attached here. I hope you all find it useful!

Just a quick disclaimer before I go: I Really DO NOT recomend that people constructing new apps use WebDAV to connect to Exchange Server. Exchange Server 2007 supports a standards-compliant Web services interface, which is much easier to use than the WebDAV interface. If you have a choice, please consider using the web services option.

Cheers!
-Dino