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.
Is it needless to say? Maybe not. So here goes: 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");
if (!server.equals(Servername))
throw new Exception ("Cross-site redirection attempt.");
PostMethod method = new PostMethod(AuthUrl);
try {
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.
<?xml version="1.0" encoding="IBM437"?>
<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-5</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" <alunj@microsoft.com></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 & budget</d:subject>
<d:from>"Marie Huwe" <mariehu@microsoft.com></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 <ItemTemplate> question</d:subject>
<d:from>"Mike Burdick" <mikebu@microsoft.com></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: