The availability information is stored in the NON_IPM_SUBTREE folder hierarchy below the Public Folders hierarchy in a folder called EX:/o=<Name of your organization>. For each administrative group, there is a subfolder. Each one of those folders contains the availability data for the users associated with that administrative group.
The question is: How do I get the right item in those folder for a particular user?
The answer comes in the form of the legacyExchangeDistinguishedName property from the Active Directory object of the user.
ADSIEdit showing the legacyExchangeDistinguishedName of a user (click to enlarge)
Once you have the legacyExchangeDistinguishedName of a user, you'll have to split it up. From position 0 to the slash ("/") before cn=Recipients and from that index to the end of the string. To construct the item url, you can use this method:
1 publicstaticstring GetFreeBusyItemUrl(string legacyExchangeDistinguishedName)
3 string url;
4 string orgFolder;
5 string ouFolder;
7 url = "http://myserver/public/NON_IPM_SUBTREE/SCHEDULE%2B%20FREE%20BUSY/EX:";
9 orgFolder = legacyExchangeDistinguishedName;
10 orgFolder = orgFolder.Substring(0, orgFolder.IndexOf("/cn=Recipients"));
11 ouFolder = legacyExchangeDistinguishedName.Substring(orgFolder.Length);
13 url += HttpUtility.UrlPathEncode(orgFolder.Replace("/", "_xF8FF_")) + "/USER-" +
14 HttpUtility.UrlPathEncode(ouFolder.Replace("/", "_xF8FF_")) + ".eml";
16 return url;
Since the legacyExchangeDistinguishedName contains a slash in the item name, you must encode it with is unicode representation, _xF8FF_, otherwise Exchange would treat it as a folder delimiter.
Once you have the url to the free/busy item, you can issue a PROPFIND on the item and retrieve these properties:
- x68531003 (Busy months)
- x68541102 (Busy events)
- x68511003 (Tentative months)
- x68521102 (Tentative events)
- x68551003 (Out of office months)
- x68561102 (Out of office events)
- x68470003 (Start of the published range)
- x68480003 (End of the published range)
All these properties live in the http://schemas.microsoft.com/mapi/proptag/ namespace.
We'll start with the last two properties. These contain the start date and the end date of the published data. These values always fall on month boundaries and are encoded in systime, a MAPI datatype.
You can use this code to convert a systime to a DateTime:
2 /// Parses a MAPI systime and converts it to a <see cref="DateTime"/> instance.
4 ///<param name="value">The value.</param>
6 privatestaticDateTime ParseSysTime(int value)
8 DateTime result;
10 result = newDateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc) + TimeSpan.FromMinutes(value);
12 return result.ToLocalTime();
The other properties contain the actual free/busy data. For each free/busy status there are two properties: One describes the months which are covered, and one contains the availability data for those months. For example, the appointments marked as busy are recorded in the properties x68531003 and x68541102. The first property has a datatype of mv.int, the latter one is a mv.binary. Both arrays should have the same length, otherwise, the free/busy data is corrupted. To decode the values, you'll have to iterate through those arrays and decode the information. You can use this code snippet to do this:
1 privatevoid ParseFreeBusyEntries(IList<int> monthValues, IList<byte> eventEntries, FreeBusyStatus freeBusyStatus)
3 BinaryReader reader;
4 DateTime monthStart;
5 DateTime start;
6 DateTime end;
8 for (int i = 0; i < Math.Min(monthValues.Count, eventEntries.Count); i++)
10 monthStart = newDateTime(monthValues[i] >> 4, monthValues[i] & 15, 1);
12 reader = newBinaryReader(newMemoryStream(eventEntries[i]));
14 while (reader.BaseStream.Length - reader.BaseStream.Position >= 4)
16 start = (monthStart + TimeSpan.FromMinutes(reader.ReadUInt16())).ToLocalTime();
17 end = (monthStart + TimeSpan.FromMinutes(reader.ReadUInt16())).ToLocalTime();
19 StoreFreeBusyEntry(start, end, freeBusyStatus);
The method requires you to extract the arrays from the xml response stream and decode the base64 encoded data to a byte array. The FreeBusyStatus type in line one is an enumeration consiting of the various free/busy status. The method iterates through the two arrays. From each entry in the monthValues array, it extracts the start date for that range. It then constructs a BinaryReader to read through the corresponding entry in the eventEntries array and extracts the event information from the stream. For each entry, the StoreFreeBusyEntry() is called to do something with the extracted information. In my library, I put them into a global list of events.
You'll have to call this method for each of the three pairs of properties. Note that not all properties are always set. If no appointment with a free/busy status of busy occurs during the published period, the two corresponding properties would be null.
This method will work with Exchange 2000 and 2003. It will also work with Exchange 2007 if you have the public folder tree installed, which is not longer mandatory.
When you create a meeting with OWA, it will display the availability information of the attendees like Outlook does. To get the information, it uses a special OWA command which returns the free/busy information as an XML stream.
Just issue this request to the server and you'll get your data:
1 GET /public/?Cmd=freebusy&start=<startDate>&end=<endDate>&interval=<interval>&u=SMTP:<userEmail>
3 Translate: t
4 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)
5 Host: <Server>
The <startDate> and <endDate> must be replaced with the date range you are requesting, in the format yyyy-MM-ddTHH :mm:sszzz. An example would be 2004-01-01T14:00:00+02:00 (January first, 2004, two PM with a time-zone of two hours).
<interval> specifies the interval in minutes. Minimum is 5.
Finally, the <userEmail> must be replaced with the SMTP email address of the user for which the free/busy information is requested. If you want to request the free/busy data for multiple persons for the same date range, you can add more &u=SMTP:<Emailaddress>. However, the maximum length for the GET command is 1024 bytes.
The following example would request the free/busy data for John Doe (firstname.lastname@example.org) for the 25 July 2004 from two PM to four PM CET (Central European Time, GMT+01:00) with an interval of 30 minutes:
1 GET /public/?cmd=freebusy&start=2004-07-25T14:00:00+01:00&end=2004-07-25T16:00:00+01:00&interval=30&u=SMTP:email@example.com
The response will look like this:
Now, the interesting information is in the <a:fbdata> node. This node contains the requested data. Each digit represents one interval. The meaning is as follows:
- 0 equals CdoFree
- 1 equals CdoTentative
- 2 equals CdoBusy
- 3 equals CdoOutOfOffice
It is important that you send that User-Agent string along. If you don't pretend to be an Internet Explorer 6 or later, you won't get an XML response because OWA thinks you are using a down-level browser which don't understands XML.
This method will also not work with Exchange 2007, as most of the OWA commands have been removed from Exchange.