Exchange 2003 meeting request submission, update and cancellation from server-side applications

I recently had a project where part of the functionality was to make meeting room booking functionality against Exchange Server 2003 (I re-invented OWA :) - no, it was just part of the application, integrated with other functionalities), and also to cancel or update the request and do free/busy lookup. It was an ASP.net application, so the application was running in an IIS worker process, meaning that I had to use server-side APIs to do the job. Otherwise, a popup message could block the server process, making my web application unresponsible. The APIs available for server-side applications are

  1. Extended MAPI (note: it's not supported from .net, so it's not a real choice)
  2. CDOEX
  3. CDOSYS
  4. WebDav
  5. ExOleDb

As you might now, I was working as a support engineer at the Exchange Development team at Microsoft GTSC-PSS, so if I’m not going to use an official documented API to do the stuff, who does it then? So I *HAVE TO* do it in a right way.

Creating, cancelling and updating a meeting request is not just a raw operation (like changing a property or copying an item), so the APIs above were shrinked down to the ones containing the business logic for these operations. This is a shorter list:

  1. CDOEX
  2. WebDav

… partially: CDOEX is OK, it has all the business logic I need, but the problem is that it can be used only on the Exchange Server, locally (otherwise, part of its functionality doesn't work). Of course, my app is running on a different box. As an alternative choice, I could have designed my application to publish the CDOEX functionality on the Exchange Server as a set of webservice methods, but I couldn't do that, due to (obvious) security and performance reasons at the Customer.

So, WebDav remains. Hmmm … not a broad list of choices! :)  Let's check the documentation around WebDav: the kb article at support.microsoft.com/?id=308373 tells you how to send a meeting request with WebDav, while support.microsoft.com/?id=920134 tells that nothing else is supported than a few operations listed there (of course, the operations needed for me are not on the list). So, WebDav is also not a good choice, as even if it *COULD* be used to create the meeting request, then later, after you sent the request, you are alone in the dark again, as it’s *SURELY* not supported to cancel or update it. The reason could be that just part of the fields needed for these operations are documented/created in the WebDav calendar schema. So, I’m in a big trouble now: there’s no way to deliver my funcionality with documented APIs. Of course, Exchange 2007 brings the nice and shiny webservices out of the box, but I can’t wait for it, or even if I could, my Customer is not going to upgrade for another 1-2 years. So, I’m in a real trouble.

Then, OWA came into my mind. Every functionality that OWA has, can be called by HTTP requests (that's the way Http works :). Of course, the way OWA handles meeting requests is OK. The only question is that if it’s supported (will it change after the next SP?) or not. Unfortunately, it's also not supported, but it's partially documented. So, this seems to be the best (and only) choice remained. You can find the OWA customization docu at www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=6532E454-073E-4974-A800-1490A7CB358F, it's a nice documentation, worth a look! So, I started reverse engineering my scenarios by using NetMon 2.0 and wrote some nice helper functions. Pasted here the simplified copies FYI. Here they are:

Cancelling a meeting request with an OWA command:

This sample shows you how to cancel a meeting that you organized with multiple attendees. It's just a sample, it's not complete, for example, the folder "Calendar" is hard coded while it could be more smart by reading up the Calendar folder's name from the mailbox root. Anyways, here we go:

 /// <summary>
/// Cancels a meeting request
/// </summary>
/// <param name="mainMeetingItemName">Meeting item name in the user's calendar</param>
public static void CancelMeetingRequest(
    string mainMeetingItemName, string exchSvrName, string mailboxName)
{
    NetworkCredential networkCredential = null;
    
    // Set the OWA server's name, the mailbox name and the user's OWA calendar Url
    string owaCalendarUrl = string.Format("{0}/exchange/{1}/calendar",
        exchSvrName, mailboxName);
    // Make sure the slash at the end
    owaCalendarUrl = MakeSureSlashAtTheEnd(owaCalendarUrl);
    // Create the request string
    string webRequest = string.Format("{0}{1}?Cmd=getcancel",
        owaCalendarUrl, mainMeetingItemName);
    // Create the request object
    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(webRequest);
    // Set the credentials based on the configuration settings
    networkCredential = CredentialCache.DefaultNetworkCredentials;
    request.Credentials = networkCredential;
    // Set up the headers
    request.Accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, " +        application/msword, */*";
    request.Headers.Add("Accept-Language", "hu");
    request.Headers.Add("Accept-Encoding", "gzip, deflate");
    request.UserAgent = 
        "Mozilla/4 0 (compatible; MSIE 6 0; Windows NT 5 1; SV1; " +        "NET CLR 1 0 3705;  NET CLR 1 1 4322;  NET CLR 2 0 50727)";
    // Send the request and get the response stream
    HttpWebResponse owaResponse =         (HttpWebResponse)request.GetResponse();
    // Retrieve and form the location of the moved Cancel message.  
    //  This message is created by OWA for sending it to the meeting attandees
    string cancelMsgLocation = "" + exchSvrName +         owaResponse.ResponseUri.AbsolutePath;
    // Build the mail submission URI of the current user for sending     // the message
    string submissionUri = "" + exchSvrName + "/exchange/" +
        mailboxName + "/##DavMailSubmissionURI##/";
    
    // Move the draft message to the submission Uri
    MoveItemWithWebDav(cancelMsgLocation, submissionUri, networkCredential);
    // Delete the meeting from the calendar
    DeleteItemWithWebDav(owaCalendarUrl + mainMeetingItemName,         networkCredential);
}

The point is to do a ?Cmd=GetCancel call on the meeting item, retrieve the Url OWA gives back to you in the webresponse (that will be the cancellation item's Url that OWA pre-created for you) and send that item out.

Updating a meeting request with OWA commands

This one is a piece of cake! You just have to do the neccessary changes on the meeting item in the organizer's calendar - probably by updating the recipients (to: field -> required attendees, cc: field -> optional attendees, bcc: field -> resources), update the start and end date/time, location of the appointment item if neccessary - as when you create a meeting request with WebDav, then run the following function to ask OWA to send an update to all recipients. Even if you empty the attendees fields (to:, cc:, bcc:), OWA will manage the attendees receive az update request containing the changes:

 /// <summary>
/// Sends an update to the attendees of a meeting request
/// </summary>
/// <param name="booking">A booking instance that has changed</param>
/// <param name="owaMailboxUrl">Url of the user's mailbox</param>
/// <param name="calendarFolderName">The user's calendar folder name</param>
public static void UpdateMeetingRecipients(
    Data.Booking booking, string owaMailboxUrl, string calendarFolderName)
{
    NetworkCredential networkCredential = null;
    // Make sure the slash at the end
    owaMailboxUrl = MakeSureSlashAtTheEnd(owaCalendarUrl);
    calendarFolderName = MakeSureSlashAtTheEnd(calendarFolderName);
    
    // Set up the calendar Url
    owaCalendarUrl = owaMailboxUrl + calendarFolderName
    // Collect the meeting item's Url
    string meetingItemUrl = owaCalendarUrl + booking.AppointmentItemName;
    // Create the request object
    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(
        meetingItemUrl);
    // Set the method and content type
    request.Method = "POST";
    request.ContentType = "application/x-www-UTF8-encoded";
    // Set the network credentials
    networkCredential = CredentialCache.DefaultNetworkCredentials;
    request.Credentials = networkCredential;
    // Set up the headers
    request.Accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, " +        "application/msword, */*";
    request.Headers.Add("Accept-Language", "hu");
    request.Headers.Add("Accept-Encoding", "gzip, deflate");
    request.UserAgent = "Mozilla/4 0 (compatible; MSIE 6 0; Windows NT 5 1; " +
        SV1; NET CLR 1 0 3705;  NET CLR 1 1 4322;  NET CLR 2 0 50727)";
    // Write to the request stream
    Stream requestStream = request.GetRequestStream();
    StreamWriter requestBodyWriter = new StreamWriter(requestStream);
    requestBodyWriter.Write("Cmd=sendappt\n");
    requestBodyWriter.Write("Required=\n");
    requestBodyWriter.Write("Optional=\n");
    requestBodyWriter.Write("Resource=\n");
    requestBodyWriter.Write("FormType=appointment\n");
    requestBodyWriter.Write(string.Format("MsgID=/{0}{1}\n",         calendarFolderName, booking.AppointmentItemName));
    requestBodyWriter.Write(        string.Format("urn:schemas:httpmail:subject={0}\n",         "Changed: " + booking.Subject));
    requestBodyWriter.Write(        string.Format("urn:schemas:httpmail:htmldescription={0}\n",
        "This is an updated meeting request));
    requestBodyWriter.Write(        string.Format("urn:schemas:calendar:dtstart={0}\n",         ConvertDateToOwaFormat(booking.Start)));
    requestBodyWriter.Write(        string.Format("urn:schemas:calendar:dtend={0}\n",         ConvertDateToOwaFormat(booking.End)));
    requestBodyWriter.Write(        string.Format("urn:schemas:calendar:location={0}\n",         booking.MeetingRoom.Name));
    requestBodyWriter.Write(        string.Format("CALENDAR:UID=/{0}{1}\n",         calendarFolderName, booking.AppointmentItemName));
    requestBodyWriter.Write(        "urn:schemas:calendar:responserequested=1\n");
    requestBodyWriter.Write(        string.Format(        "schemas.microsoft.com/exchange/subject-utf8=%7B0%7D/n",         booking.Subject));
    // Flush the stream
    requestBodyWriter.Flush();
    requestBodyWriter.Close();
    // Send the request
    HttpWebResponse owaResponse = (HttpWebResponse)request.GetResponse();
}

This is nice, isn't it? :) 

Creating the meeting request 

For creating a meeting request, I used the documented way with WebDav, which seems to be also not supported: support.microsoft.com/?id=308373

Summary 

Don't forget that the examples shown above are unsupported and they may won't work with the next service pack of Exchange Server 2003. If you have a chance, use CDOEX on the Exchange Server by exposing it as a set of webservices. Otherwise, you could use this unsupported thing that I mentioned above, but if there's anything going wrong, you can't go to Microsoft Product Support for help. Feel free to post any questions here, if there's a problem with it.