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
- Extended MAPI (note: it's not supported from .net, so it's not a real choice)
- CDOEX
- CDOSYS
- WebDav
- 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:
- CDOEX
- 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.
Comments
Anonymous
November 23, 2006
Hi. I have a strange problem: I can send the meeting request cancelation and the client gets the cancelation mail with "remove from calendar" option but the appointment doesn't get removed. i tried your method but it doesn't work. The cancelMsgLocation is the same as the original appointment URI i'm trying to cancel. is that normal? because the mail i send is an appointment and not a cancelation. I can't believe that cancelation and updating can't be done with WebDAV. It's ridicolous...Anonymous
November 28, 2006
BTW. this whole OWA procedure is completly unnecessary. you have to send the meeting request like i described here: http://weblogs.sqlteam.com/mladenp/articles/9560.aspx in Life of an item in WebDAV generation process and problems that arise - 3. Meeting Requests and if you set the urn:schemas:calendar:uid to some value then the meeting request cancelatation is simple.
- take you appointment to which you've already set the UID.
- set http://schemas.microsoft.com/exchange/outlookmessageclass = "IPM.Schedule.Meeting.Canceled" urn:schemas:mailheader:content-class = "urn:content-classes:calendarmessage" urn:schemas:calendar:method = "Cancel"
- proppatch that
- move the appointment to ##DavSubmissionURI## update is a maater of updating the appointment and sending another meeting request.
Anonymous
November 28, 2006
Hello, Sorry for the late reply. No, it's not normal if the cancelMsgLocation is the same Url as the original appointment, as it should be a meeting cancellation item pre-created by OWA for you to send it out, in your Drafts folder. This is typically the cancel message that you can edit before sending out the cancellation, this is why it takes 2 step to do it. Are you sure that you are reading out the correct URL from OWA's response? If you do the cancellation with OWA on the UI, does it work? Maybe, the creation of the meeting request went wrong? If cancelling a meeting request you created with you application works with OWA from the UI, trace the network activity while doing it, then look up the cancellation packets and you'll find what's wrong with your code. The reason I used OWA was that (1) you don't have to reverse engineer how meeting cancellations work inside, OWA does the job for you, you just have to specify the input parameters and the business logic works fine, and (2) because WebDav meeting cancellation is also not supported (its schema is not fully documented) and the Exchange dev team more suggests the OWA way to do it if needed.Anonymous
March 28, 2007
Hi, I have two different exchange organizations in different forest and active directory. Let's call it A and B. I use outlook or OWA to create meeting requests from A Org to users in B Org. These organizations depend on internet or smart host to communicate. I want to how these meeting are carried between each organization ? I am obeserving different times when they are scheduling meetings:( How do I track, these meeting sent from A to B or B to A over internet ? any inputs appreciatedAnonymous
April 07, 2007
Hello Manju,The meeting requests are traveling in an ICAL standard format if you do them using the OWA APIs or WebDav.
The free/busy part is another question (e.g. - how you look up the free/busy) - you have to solve this by looking up different free/busy URLs.
The different start times could be caused by different timezone settings in OWA. You can check it on the OWA control panel for your mailboxes. David
Anonymous
April 13, 2007
Hi David ! You´re article is really nice ! Is there a way my ASP.NET application can create meetings for different users, using only one credential for authentication againts exchange ?Anonymous
April 14, 2007
Hello Mauricio, Create a Credential object by specifying your system account's name and share this object among web sessions (Session object) - not sure if the Credential object is thread safe, but I expect it is. You could also try sharing the HttpWebRequest object. DavidAnonymous
August 07, 2007
Thanks a lot David, very useful. Your examples show to send requests from the meeting ORGANIZER's mailbox. Can you dive an idea on how to submit accept / decline / propose new time requests from an ATTENDEE mailbox?Anonymous
August 07, 2007
Hi Joseph, Take a look at the following page: http://msdn2.microsoft.com/en-us/library/aa204771(office.11).aspx. This will help you in searching for the appropriate message classes - I'm sure you will find a few samples.Anonymous
August 07, 2007
Thanks a lot again David, Using message class means using Mladen's approach with PROPPATCH, right? Is there an OWA way? When I've tried Mladen's approach, I got the cancellation email delivered to all the attendees, including the organizer - what could I be doing wrong? And I did not find a message class for the "propose new time" request :-(Anonymous
August 14, 2007
The comment has been removedAnonymous
June 27, 2008
I have built a complete API on Sending/Cancelling/Updating meeting requests with WebDAV. It supports freebusy lookup too. EMail me at khanrocks@gmail.com to know more about it.Anonymous
June 27, 2008
I can't provide you the code as it is confidential but I can certainly help on this topic. So If you have any questions please email me.Anonymous
November 04, 2008
Hi everybody, I want to implement an api to access the Exchange Server's Conference room booking without using outlook. The implemented API should be worked in a device with Window CE ( a thin OS). The API should get, uppdate, delete the Conference booking rooms. If some body is created a new booking by outlook, then the new booking should be displayed on the devise display. Best regards /MehrdadAnonymous
May 29, 2009
I've tried this several times (the udpate) and it's not working. I receive a 200 response the meeting update never gets sent. I've traced it using fiddler and the request is identical to the one in OWA. Here's my code: Dim meetingItemUrl As String Dim request As Net.HttpWebRequest Dim MyCredentialCache As System.Net.CredentialCache Dim requestStream As IO.Stream Dim owaResponse As Net.HttpWebResponse Dim s As New Text.StringBuilder Dim bytes() As Byte meetingItemUrl = strCalendarUri + owaAppointmentItemName & ".eml" MyCredentialCache = New System.Net.CredentialCache MyCredentialCache.Add(New System.Uri(strCalendarUri), "BASIC", New System.Net.NetworkCredential(My.Settings.mail_owa_username, My.Settings.mail_owa_password)) request = Net.HttpWebRequest.Create(meetingItemUrl) request.Method = "POST" request.ContentType = "application/x-www-UTF8-encoded" request.Credentials = MyCredentialCache request.Accept = "/" request.Headers.Add("Accept-Language", "en-us") request.Headers.Add("Accept-Encoding", "gzip, deflate") request.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)" requestStream = request.GetRequestStream() s.AppendLine("Cmd=sendappt") s.AppendLine("Required=") s.AppendLine("Optional=") s.AppendLine("Resource=") s.AppendLine("FormType=appointment") s.AppendLine(String.Format("MsgID=/Calendar/{0}", owaAppointmentItemName & ".eml")) s.AppendLine(String.Format("urn:schemas:httpmail:subject={0}", v_subject)) s.AppendLine(String.Format("urn:schemas:calendar:location={0}", "Changed location")) s.AppendLine(String.Format("CALENDAR:UID=/Calendar/{0}", owaAppointmentItemName & ".eml")) s.AppendLine("urn:schemas:calendar:responserequested=1") s.AppendLine(String.Format("http://schemas.microsoft.com/exchange/subject-utf8={0}", v_subject)) s.AppendLine(String.Format("urn:schemas:httpmail:textdescription={0}", "This is an updated meeting request")) bytes = System.Text.Encoding.UTF8.GetBytes(s.ToString) requestStream.Write(bytes, 0, bytes.Length) requestStream.Flush() requestStream.Close() owaResponse = request.GetResponse owaResponse.Close()Anonymous
March 24, 2010
First great article. I don't know if you can help me but do you know a link that has more help on WebDav? I manage to create a an Appointment (WebDAV) but if you setup anther appointment at the same time it overwrite my first appointment. I rather have both appointment. do you have the same problem? I used that code: http://msdn.microsoft.com/en-us/library/ms877306(EXCHG.65).aspxAnonymous
December 07, 2011
Thanks for your post. I am not able to use your code in exchange server 2003 currently. is it still valid? i am able to create appointment using webdav but not able to send invites to the attendees. Any suggestions are welcome. My issue is as explained below. social.msdn.microsoft.com/.../88ccc151-3525-4c87-850d-fafb13f4de73