InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

Exchange 2007 Push notifications made easy

Exchange 2007 de-emphasized Exchange Event Sinks in favor of a new notification system which allows an application to receive notifications about changes made to items on a public folder or mailbox folder. There are two types of notifications: Pull and Push. With pull notifications, the calling application is responsible to get changes back from the server (it has to poll the CAS role for changes). Push notifications on the other side use Web Services to call into an external application. There are a restrictions where this does not work (for example firewalls blocking traffic), but otherwise they work really well.

Compared to the WebDAV notifications that where available in earlier versions of Exchange, they have the advantage that the external application gets specific information about the event: Which item has been modified, created, deleted?

However, it’s rather complicated to completely implement a listener for these notifications. Therefore, I have created a wrapper which uses WCF to listen for incoming notifications.

The whole package is available from CodePlex (http://www.codeplex.com/exchangenotification), and there is a list of features. Only the source and a small help file for now, but I will add samples over time.

If you have feedback, please use CodePlex Discussions or send me a mail by using the contact form.


Posted by Henning Krause on Tuesday, December 30, 2008 1:51 PM, last modified on Monday, November 29, 2010 6:03 PM
Permalink | Post RSSRSS comment feed

ExchangeWebServices / WebDAV and untrusted server certificates

Exchange 2007 has requires SSL for its WebServices, and event for Exchange 2003 some administrators have enabled this requirement on the IIS. If you are dealing with a self-signed certificate on the server and want to use .NET, you will stumble across this error message:

The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

or

The remote certificate is invalid according to the validation procedure.

By default, .NET checks whether SSL certificates are signed by a certificate from the Trusted Root Certificate store. To override this behavior, use the System.Net.ServicePointManager.ServerCertificateValidationCallback property:

   1: ServicePointManager.ServerCertificateValidationCallback = RemoteCertificateValidationCallback;

The callback looks like this:

   1: private static bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
   2: {
   3:     return true;
   4: }

This will accept all certificates, regardless of why they are invalid. One option here is to display a warning similar to the Internet Explorer one.

Using C# 3.0, this can even be written with less code:

   1: ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;

Posted by Henning Krause on Wednesday, November 26, 2008 5:57 PM, last modified on Wednesday, November 26, 2008 5:57 PM
Permalink | Post RSSRSS comment feed

Event 1503: OMA DirectoryNotFoundException

I just stumbled across this error message:

Event Type:    Fehler
Event Source:    MSExchangeOMA
Event Category:    (1000)
Event Id:    1503
Date:        11/26/2008
Time:        14:11:35
User:        Not Applicable
Server:    servername
Description:
Method not found: System.String System.DirectoryServices.DirectoryEntry.get_Password().

Source: Microsoft.Exchange.OMA.Preferencing

Stacktrace:
   
   at Microsoft.Exchange.OMA.Preferencing.AdUserObject.get_globalWirelessEnable()
   at Microsoft.Exchange.OMA.Preferencing.AdUserObject.get_UserIsWirelesslyEnabled()
   at Microsoft.Exchange.OMA.Preferencing.OmaUserInfo.get_UserIsWirelesslyEnabled()
   at Microsoft.Exchange.OMA.UserInterface.Global.Session_Start(Object sender, EventArgs e)

Meldung: Eine Ausnahme vom Typ Microsoft.Exchange.OMA.DataProviderInterface.ProviderException wurde ausgelöst.
Ereignismeldung: 
Benutzermeldung: Systemfehler at der Verarbeitung Ihrer Anforderung. Versuchen Sie es erneut. Wenden Sie sich an Ihren Administrator, wenn das Problem wiederholt auftritt.
Quelle: Microsoft.Exchange.OMA.UserInterface
Stacktrace:
   at Microsoft.Exchange.OMA.UserInterface.Global.Session_Start(Object sender, EventArgs e)
   at System.Web.SessionState.SessionStateModule.RaiseOnStart(EventArgs e)
   at System.Web.SessionState.SessionStateModule.CompleteAcquireState()
   at System.Web.SessionState.SessionStateModule.BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData)
   at System.Web.HttpApplication.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

OMA apparently tries to read the password from a System.DirectoryServices.DirectoryEntry instance. When taking a look at the documentation, one soon finds that the Password property is write-only in .NET 2.0, but was read/write in .NET 1.1.

Solution

Configure the WebSite to run with .NET 1.1. You may need to install it on the server first. If other applications are running on the IIS, be sure to move them to another application pool, if they need .NET 2.0.


Posted by Henning Krause on Wednesday, November 26, 2008 5:43 PM, last modified on Wednesday, November 26, 2008 5:43 PM
Permalink | Post RSSRSS comment feed

EventSink Registration WHERE Problems

A reader recently asked how he could register an Exchange 2000/2003/2007 EventSink so that it would only fire for items with these properties:

  • Not hidden
  • An appointment item.
  • The appointment is not private/personal/confidential.
  • The appointment is labeled blue/business/label (this is stored on property http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-C000-000000000046}/0x8214 which is of type int. The required value is 2).

He tried registering the EventSink with the following restriction for the RegEvent.vbs script:

"WHERE $DAV:ishidden$ = FALSE AND $DAV:contentclass$ = 'urn:content-classes:appointment' AND $http://schemas.microsoft.com/mapi/sensitivity$ = 0 AND $http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-C000-000000000046}/0x8214$ = 2"

This didn’t work. The problem is that Exchange is not very smart about guessing the types of properties which are not in it’s schema. It treats them all as strings. So, while a simple comparison is sufficient for the sensivity property, the appointment color property must be cast to int.

The correct where clause is this:

"WHERE $DAV:ishidden$ = FALSE AND $DAV:contentclass$ = 'urn:content-classes:appointment' AND $http://schemas.microsoft.com/mapi/sensitivity$ = 0 AND Cast($http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-C000-000000000046}/0x8214$ as int) = 2"

Posted by Henning Krause on Friday, October 24, 2008 5:22 PM, last modified on Friday, October 24, 2008 5:22 PM
Permalink | Post RSSRSS comment feed

Exchange Eventsink Foundation

Although Eventsinks are being removed from the next version of Exchange (see Exchange Developer Roadmap - WebDAV and StoreEvents gone), they are widely used and sometimes the only option. Especially when working with tasks, contacts and appointments.

But writing Eventsinks has always been a pain because all this interop stuff, COM+ registration issues and more. Attached to this post you can find a small framework which simplifies writing event sinks (both synchronous and asynchronous ones).

The InfiniTec.Exchange.Eventing assembly contains several base classes from which you inherit your Eventsink. The only thing you have to do is to overwrite some methods (OnItemChanged, OnItemCreated, OnItemCreated) and you get relevant information about the event which happened in a compact object (EventInfo class). Below is a class diagram of the InfiniTec.Exchange.Eventing namespace:

image

Depending whether you want to create an asynchronous or synchronous Eventsink, you derive your sink from either SynchronousEventSink or AsynchronousEventSink. In you derived class, you simply override the notifications you want to catch. One thing all Eventsinks have in common is the registration process – Implemented by the OnRegisteringEvent. By returning false from this method you can prevent the registration on a certain url.

The synchronous Eventsink is called two times for each event: The first execution is called the Begin Phase; during this stage, the item is writable and you can even check for changes by loading the original item (via EventInfo.OpenItemLocally()). The seconds stage is called Commit Phase; the item is read-only now. If you need to share information about a particular event between these to stages, set the EventInfo.UserState property. The object graph is serialized using the BinarySerializer during the begin phase, so each instance used here must be serializable. This is a feature which was not possible with the interop files generated with tlbimp, because that tool did not generate the proper interop code. Since I’ve incorporated the complete interop code in this assembly, I fixed the signature of the affected interface.

Correctly deciphering the flags passed to the Evensink was an art of it’s own, so I’ve cleaned up those flags enumerations as well and routed them different methods (OnItemCreated with its CreationMode parameter and OnItemDeleted with its DeletionMode parameter).

At last, you don’t need the the regevent.vbs script any longer because I’ve included an EventSinkInstaller which registers your EventSink on a certain folder.

Here is a sample event sink:

   1: using System;
   2: using System.Runtime.InteropServices;
   3: using ADODB;
   4: using InfiniTec.Exchange.Eventing;
   5:  
   6: namespace TestSink
   7: {
   8:     [ComVisible(true)]
   9:     [Guid("FD0D03A3-9FD2-432b-B331-E7C4D412827F")]
  10:     [ProgId("TestSink.TestSink")]
  11:     public class TestSink: SynchronousEventSink
  12:     {
  13:         protected override void OnItemCreating(EventInfo info, CreationMode creationMode)
  14:         {
  15:             // Do something of interest here - you have read/write  access to the item
  16:             Record item = info.Item;
  17:  
  18:             // you can save an object here (it must be serializable) and reuse it during the OnItemCreated method
  19:             info.UserState = "test";
  20:             
  21:  
  22:             base.OnItemCreating(info, creationMode);
  23:         }
  24:  
  25:         protected override void OnItemCreated(EventInfo info, CreationMode creationMode)
  26:         {
  27:             base.OnItemCreated(info, creationMode);
  28:  
  29:             // Do something of interest here - the item is readonly now.
  30:             
  31:             // will be "test"
  32:             string s = (string) info.UserState;
  33:         }
  34:  
  35:  
  36:         protected override void OnItemDeleted(EventInfo info, DeletionMode deletionMode)
  37:         {
  38:             base.OnItemDeleted(info, deletionMode);
  39:         }
  40:  
  41:         protected override void OnItemUpdated(EventInfo info)
  42:         {
  43:             base.OnItemUpdated(info);
  44:         }
  45:  
  46:         protected override void OnErrorOccured(EventInfo eventInfo, Exception ex)
  47:         {
  48:             base.OnErrorOccured(eventInfo, ex);
  49:         }
  50:  
  51:         protected override void OnInitialize()
  52:         {
  53:             base.OnInitialize();
  54:         }
  55:  
  56:         protected override bool OnRegisteringEvent(EventInfo info)
  57:         {
  58:             return base.OnRegisteringEvent(info);
  59:         }
  60:  
  61:         protected override void OnItemCreationAborted(EventInfo info, CreationMode creationMode)
  62:         {
  63:             base.OnItemCreationAborted(info, creationMode);
  64:         }
  65:  
  66:         protected override void OnItemDeleting(EventInfo info, DeletionMode deletionMode)
  67:         {
  68:             base.OnItemDeleting(info, deletionMode);
  69:         }
  70:  
  71:         protected override void OnItemDeletionAborted(EventInfo info, DeletionMode deletionMode)
  72:         {
  73:             base.OnItemDeletionAborted(info, deletionMode);
  74:         }
  75:  
  76:         protected override void OnItemSilentlySaving(EventInfo info)
  77:         {
  78:             base.OnItemSilentlySaving(info);
  79:         }
  80:  
  81:         protected override void OnItemUpdateAborted(EventInfo info)
  82:         {
  83:             base.OnItemUpdateAborted(info);
  84:         }
  85:  
  86:         protected override void OnItemUpdating(EventInfo info)
  87:         {
  88:             base.OnItemUpdating(info);
  89:         }
  90:     }
  91: }

As I did here, you should always add those three attributes to the class, albeit with different values for the Guid and ProgId attribute. And you should add these lines to your assemblyinfo file:

   1: [assembly: ApplicationActivation(ActivationOption.Server)]
   2: [assembly: ApplicationName("TestSink")]
   3: [assembly: ApplicationAccessControl(false, AccessChecksLevel = AccessChecksLevelOption.Application)]

Be sure to customize the ApplicationName attribute, though.

Now that you have created an Eventsink, you should add an installer so you can deploy the solution more easily:

   1: using System.Collections;
   2: using System.ComponentModel;
   3: using System.Configuration.Install;
   4: using InfiniTec.Exchange.Eventing;
   5:  
   6: namespace TestSink
   7: {
   8:     [RunInstaller(true)]
   9:     public class TestInstaller: Installer
  10:     {
  11:         public override void Commit(IDictionary savedState)
  12:         {
  13:             Initialize();
  14:             base.Commit(savedState);
  15:         }
  16:  
  17:         public override void Install(IDictionary stateSaver)
  18:         {
  19:             Initialize();
  20:             base.Install(stateSaver);
  21:         }
  22:  
  23:         public override void Rollback(IDictionary savedState)
  24:         {
  25:             Initialize();
  26:             base.Rollback(savedState);
  27:         }
  28:  
  29:         public override void Uninstall(IDictionary savedState)
  30:         {
  31:             Initialize();
  32:  
  33:             base.Uninstall(savedState);
  34:         }
  35:  
  36:         private void Initialize()
  37:         {
  38:             var url = Context.Parameters["url"] + "/eventsink.evt";
  39:  
  40:             Context.LogMessage("Binding Url: " + url);
  41:             string criteria = Context.Parameters["Criteria"] ?? string.Empty;
  42:             criteria = criteria.Replace("$", "\"");
  43:  
  44:             Installers.Add(new EventSinkInstaller<TestSink>
  45:                                {
  46:                                    Criteria = criteria,
  47:                                    EventMethods = EventMethods.SynchronousEvents,
  48:                                    Scope = MatchScope.Shallow,
  49:                                    Url = url
  50:                                });
  51:         }
  52:     }
  53: }

The reason why the Initialize() method is called in each of the methods is that the installer requires the url to the path on which the sink is registered. Unfortunately, the Context property is set after the constructor is run. What you basically need to do is  to add an EventSinkInstaller the list of installers executed. The EventSinkInstaller has a type parameter which you can use to specify the type of sink registered. The installer will use this type parameter to determine the correct ProgId for the Eventsink. This example registers a synchronous event with a MatchScope of Shallow on the folder specified via the “url” parameter. Additionally, the constraint for the Eventsink is taken from the “constraint” parameter. This approach is superior to the regevent.vbs script because it lets you as the developer decide which parameters are fixed (the scope and type of the EventSink in this case) and which are variable, making the whole registration purpose less prone to errors.

Debugging

The InfiniTec.Exchange.Eventing assembly also defines a TraceSource which you can use for debugging. The TestSink application attached to this article contains the following application.config to activate the trace component:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.diagnostics>
   4:         <trace autoflush="true" />
   5:         <sources>
   6:             <source name="infinitec.exchange.eventing"
   7:               switchName="verbosity"
   8:               switchType="System.Diagnostics.SourceSwitch">
   9:         <listeners>
  10:           <add name="listener"
  11:            type="System.Diagnostics.TextWriterTraceListener"
  12:            initializeData="c:\temp\TextWriterOutput.log" />
  13:         </listeners>
  14:       </source>
  15:         </sources>
  16:         <switches>
  17:             <add name="verbosity" value="All"/>
  18:         </switches>
  19:     </system.diagnostics>
  20: </configuration>

Licensing

You may use this code (InfiniTec.Exchange.Eventing and InfiniTec.Common) in your application, regardless whether it is personal or business, free of charge.

Downloads


Tags: , ,

Technorati: , ,

Posted by Henning Krause on Tuesday, July 22, 2008 5:16 PM, last modified on Tuesday, July 22, 2008 5:18 PM
Permalink | Post RSSRSS comment feed

Retrieving the size of a mailbox via WebDAV

A common question is how to retrieve the size of a given mailbox or the size of a folder structure via WebDAV. There is an example for this task on MSDN (Getting the Size of a Mailbox (WebDAV)). But that example is rather difficult to understand because the WebDAV requests are built using simple string concatenation, which is rather ugly. It is far better to create those requests using the XmlReader and XmlWriter classes (I’ve written about it in the article PROPPATCH requests using XmlReader and XmlWriter, Part 1).

The procedure to retrieve the cumulative folder size is this:

  • Issue a SEARCH request on the root folder of interest (for example the mailbox) and retrieve the size of all messages in that folder
  • Repeat this step for each sub folder.

Here is a C# program which demonstrates this:

   1: using System;
   2: using System.IO;
   3: using System.Net;
   4: using System.Text;
   5: using System.Xml;
   6: using System.Xml.XPath;
   7:  
   8: namespace InfiniTec.Exchange.Examples
   9: {
  10:     internal class Program
  11:     {
  12:         private const string DavNamespace = "DAV:";
  13:         private const string ProptagNamespace = "http://schemas.microsoft.com/mapi/proptag/";
  14:  
  15:  
  16:         public static byte[] GetFolderSizeRequest(string url)
  17:         {
  18:             var settings = new XmlWriterSettings {Encoding = Encoding.UTF8};
  19:  
  20:             using (var stream = new MemoryStream())
  21:             using (XmlWriter writer = XmlWriter.Create(stream, settings))
  22:             {
  23:                 writer.WriteStartElement("searchrequest", DavNamespace);
  24:                 var searchRequest = new StringBuilder();
  25:  
  26:                 searchRequest.AppendFormat("SELECT \"http://schemas.microsoft.com/mapi/proptag/x0e080014\", \"DAV:hassubs\" FROM SCOPE ('HIERARCHICAL TRAVERSAL OF \"{0}\"')", url);
  27:  
  28:                 writer.WriteElementString("sql", searchRequest.ToString());
  29:                 writer.WriteEndElement();
  30:                 writer.WriteEndDocument();
  31:  
  32:                 writer.Flush();
  33:                 return stream.ToArray();
  34:             }
  35:         }
  36:     
  37:         private static long GetMailboxSize(string url, ICredentials credentials)
  38:         {
  39:             XmlReader reader;
  40:  
  41:             byte[] buffer = GetFolderSizeRequest(url);
  42:  
  43:             var request = (HttpWebRequest) WebRequest.Create(url);
  44:             request.Method = "SEARCH";
  45:             request.ContentType = "text/xml";
  46:             request.Credentials = credentials;
  47:             request.Headers.Add("Translate", "f");
  48:             request.Headers.Add("Depth", "1");
  49:             
  50:             using (Stream stream = request.GetRequestStream())
  51:             {
  52:                 stream.Write(buffer, 0, buffer.Length);
  53:             }
  54:  
  55:             using (WebResponse response = request.GetResponse())
  56:             {
  57:                 string content = new StreamReader(response.GetResponseStream()).ReadToEnd();
  58:  
  59:                 reader = XmlReader.Create(new StringReader(content));
  60:  
  61:                 var nsmgr = new XmlNamespaceManager(reader.NameTable);
  62:                 nsmgr.AddNamespace("dav", DavNamespace);
  63:                 nsmgr.AddNamespace("e", ProptagNamespace);
  64:  
  65:                 var doc = new XPathDocument(reader);
  66:                 long result = 0;
  67:                 
  68:                 foreach (XPathNavigator element in doc.CreateNavigator().Select("//dav:response[dav:propstat/dav:status = 'HTTP/1.1 200 OK']", nsmgr))
  69:                 {
  70:                     var size = element.SelectSingleNode("dav:propstat/dav:prop/e:x0e080014", nsmgr).ValueAsLong;
  71:                     string folderUrl = element.SelectSingleNode("dav:href", nsmgr).Value;
  72:  
  73:                     Console.WriteLine("Folder size of {0}: {1:0.00} MB", folderUrl, (double)size / 1048576);
  74:  
  75:                     result += size;
  76:                     bool hasSubs = element.SelectSingleNode("dav:propstat/dav:prop/dav:hassubs", nsmgr).ValueAsBoolean;
  77:  
  78:                     if (hasSubs)
  79:                     {    
  80:                         result += GetMailboxSize(folderUrl, credentials);
  81:                     }
  82:                 }
  83:  
  84:                 return result;
  85:             }
  86:         }
  87:  
  88:         private static void Main()
  89:         {
  90:             long size = GetMailboxSize("http://w2k3srv.contoso.local/exchange/administrator/", new NetworkCredential("administrator", "password"));
  91:  
  92:             Console.Out.WriteLine("Mailboxsize = {0:0.00} MB",(double) size/1048576);
  93:  
  94:             Console.Out.WriteLine("Finished");
  95:             Console.ReadLine();
  96:         }
  97:     }
  98: }

Posted by Henning Krause on Wednesday, June 11, 2008 7:19 PM, last modified on Wednesday, June 11, 2008 7:20 PM
Permalink | Post RSSRSS comment feed

Exchange Developer Roadmap - WebDAV and StoreEvents gone

Microsoft just released a post about the technologies being removed from the next version of Exchange. I'm ok with WebDAV being removed, given that EWS will be extended to support access to hidden messages and providing strong-typed access to mailbox settings. I can also live with the fact that CdoEx and ExOleDB will be remove. But store events are another thing.

Sure, they are not really easy to implement and the whole access using ADO is a terrible mess. CdoEx makes it not exactly better as there is no support for tasks.

The proposed replacement for store event sinks are transport agents and EWS notification. While transport agents are fine when dealing with email messages, they are useless with respect to appointments, contacts and tasks. This leaves the EWS notifications as the sole option here. Why is this bad?

  • Synchronous execution (access to old data as well): Synchronous event sinks (OnSyncSave, OnSyncDelete) provide direct access to the item being modified (the record is directly available). And during the Begin phase, the event sink can even open the old record and execute actions based on how fields where changed. This feature will be lost completely with EWS notifications.
  • Register once, even for new users (storewide eventsinks): Store wide event sinks are registered once on a store and it will be triggered for every mailbox in the store - even new users. EWS notifications must be registered for each mailbox and the application receiving the mails is required to monitor Exchange for new mailboxes.
  • Access to all properties, even during deletion of an object: With a synchronous OnSyncDelete event sink, all properties of an item can be examined before it gets deleted. With notifications I merely get a notification that and item with a specific ItemId has been deleted. The client application is responsible to track the deleted item - whether is was soft-deleted or moved to the recycle bin. The properties can then be accessed from there. But if the item was hard-deleted (in case of the dumpster being disabled on public folders, for example), one is out of luck. The real problem is this: The ItemId is based on the email address of the mailbox owner as well as the MAPI EntryId of the item (see Exchange 2007 SP1 Item ids). Both, the mailbox address as well as the MAPI entry id are not guaranteed to be stable (Since Exchange 2003 SP2, Exchange recreates meeting items under certain circumstances: CDOEX Calendaring Differences Between Exchange 2003 and Exchange 2003 SP2). This has the effect that the ItemId is not suitable as a long term identifier which should be stored in a database. In scenarios where Exchange data is being replicated to a relational databases, this can become a problem.
  • Order of execution: Synchronous event sinks are called in the order the items were changed. With asynchronous notifications, this cannot be guaranteed.
    To clarify this: The order in which notifications are sent to the client application cannot guaranteed to reflect the order in which the changes were made. To work around this issue, the each notification carries a watermark and a previous watermark. This way a client application can restore the correct order. But with synchronous store events, this comes for free.
  • Silent updates: Due to the possibility to modify an item while it's being changed, the synchronous store events allow some sort of silent update. This works like this:
    1. Along with all the other modifications, a client program sets a special user defined field to an arbitrary value.
    2. The event sink checks this field during the Begin phase, and if it's set it won't process the item. Instead it just removes the property from the element.
  • Modify changes made by a user / Block modifications: A synchronous event sink can modify changes while they are being saved. It can even abort the change, thus preventing a user from deleting an item for example.
    Note: Canceling updates/deletions can break synchronization with ActiveSync or Outlook. So don't do this!
  • Performance: In most scenarios, the WebService receiving the push notifications will not reside on the Exchange server (I know a bunch of administrators who even hesitate to install the .NET Framework on the Exchange server let alone software which does not come from MS*). In this case, the push notifications work like this:
    image
    This makes three network hops to get the properties of a changed item. What makes things worse is the fact that the notifications are send for each modified element. With store event sinks, one could specify a filter so that the sink was only triggered for a subset of elements.

So, while store event sinks are certainly no unconfined pleasure to use, they are far more suited for certain scenarios. I would rather see them being replaced by a managed solution (like transport agents) than the stuff that is coming now.

By the way, the OnTimer, OnMdbShutdown, OnMdbStartup event sinks are being removed without any replacement.

Enough ranting for one day...

* Now, one could argue that a store event sink has far more impact on the Exchange server and those administrators would also hesitate to install them... while that is technically true, there is no other option yet, so they have to swallow that pill.


Posted by Henning Krause on Friday, May 23, 2008 4:41 PM, last modified on Tuesday, July 19, 2011 8:58 AM
Permalink | Post RSSRSS comment feed

Working with the MasterCategoryList Via WebDAV

Before Outlook 2007 categories were plain-text. They had a name, but nothing more. Outlook 2007 enhanced this concept by adding colors to categories:

 image

Each category can be given a color (one of 25) and a shortcut (CTRL+F2 through CTRL+F12). Unlike in former Outlook versions, this list of categories is no longer stored in the registry, but in the default calendar of the mailbox. Outlook 2007 creates a hidden message in that folder with the message class IPM.Configuration.CategoryList. The category configuration is stored in the MAPI property 0x7C08 type bin.base64. To query the master category list via WebDAV, two steps are necessary:

  1. Get the URL of the default calendar of a given mailbox (See Getting Well-Known Mailbox Folder URLs on MSDN).
  2. Search the default calendar for items with a message class of IPM.Configuration.CategoryList.
  3. Parse the binary stream

Getting the default calendar

To accomplish the first step, send a PROPFIND request to the root url of the mailbox:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <propfind xmlns="DAV:">
   3:     <prop>
   4:         <a:calendar xmlns:a="urn:schemas:httpmail:" />
   5:     </prop>
   6: </propfind>

The result might look like this:

   1: <?xml version="1.0"?>
   2: <a:multistatus xmlns:d="urn:schemas:httpmail:" xmlns:a="DAV:">
   3:     <a:response>
   4:         <a:href>http://server/exchange/mailbox/</a:href>
   5:         <a:propstat>
   6:             <a:status>HTTP/1.1 200 OK</a:status>
   7:             <a:prop>
   8:                 <d:calendar>http://server/exchange/mailbox/Calendar</d:calendar>
   9:             </a:prop>
  10:         </a:propstat>
  11:     </a:response>
  12: </a:multistatus>

Searching for the hidden message

Given the correct calendar folder path, a SEARCH query can be constructed to retrieve the property in question from the hidden message:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <searchrequest xmlns="DAV:">
   3:     <sql>
   3:         SELECT 
   4:             "http://schemas.microsoft.com/mapi/proptag/x7c080102", 
   5:             "http://schemas.microsoft.com/exchange/permanenturl" 
   6:             FROM SCOPE ('SHALLOW TRAVERSAL OF "http://server/exchange/mailbox/Calendar"') 
   7:             WHERE 
   8:                 ("DAV:isfolder" = false AND 
   9:                 ("http://schemas.microsoft.com/mapi/proptag/x001a001f" = 'IPM.Configuration.CategoryList')) 
  11:     </sql>
  12: </searchrequest>

adssdfdsf

If successful, the server returns something similar like this:

   1: <?xml version="1.0"?>
   2: <a:multistatus xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" 
   3:                xmlns:d="http://schemas.microsoft.com/mapi/proptag/" 
   4:                xmlns:e="http://schemas.microsoft.com/exchange/" 
   5:                xmlns:a="DAV:">
   6:     <a:contentrange>0-0</a:contentrange>
   7:     <a:response>
   8:         <a:href>http://server/exchange/mailbox/Calendar/IPM.Configuration.CategoryList.EML</a:href>
   9:         <a:propstat>
  10:             <a:status>HTTP/1.1 200 OK</a:status>
  11:             <a:prop>
  12:                 <d:x7c080102 b:dt="bin.base64">Base64 content ommited to improve readability</d:x7c080102>
  13:                 <e:permanenturl>http://server/exchange/mailbox/-FlatUrlSpace-/155ae68de4aeda4987585833042471bc-8a5e2/ac5d26d07c067f4ea4f38a18f64786a3-3a1c8a</e:permanenturl>
  14:             </a:prop>
  15:         </a:propstat>
  16:     </a:response>
  17: </a:multistatus>

sdfssdfsdfdsf

Decoding the Property value

The content can now be extracted by loading the result into a System.Xml.XmlDocument or System.Xml.Linq.XDocument and select the appropiate node using an XPath expression. To decode the value, use the System.Convert.FromBase64String and the System.Text.Encoding.UTF8.GetString method. This will yield the XML representation of the Master Category List. Since manipulation of raw XML data is not exactly fun, I've created a sample solution which uses the System.Xml.Serialization.XmlSerializer to construct a strong-typed object from the stream. Below is a class diagram of the available classes:

image

The MasterCategoryList has a static Load method which takes a System.IO.TextReader instance and reads the the value decoded earlier:

   1: using (var reader = new StringReader(masterCategoryListContent))
   2: {
   3:     var masterCategoryList = MasterCategoryList.Load(reader);
   4:     foreach (var category in masterCategoryList.Categories)
   5:     {
   6:         Console.Out.WriteLine("{0}: Color {1}, Shortcut {2}", category.Name, category.Color, category.KeyboardShortcut);
   7:     }
   8: }

This code assumes that the string representation of the Master Category List is stored in the masterCategoryListContent field.

Once done with manipulating the instance, it can be saved to a System.IO.TextWriter. The Master Category List in the mailbox can then be updated using a PROPPATCH request on the address used above.

Download: Source files

Links

Fellow MVP Glen Scales has an article on his blog about this topic as well: Adding Categories to the Master categories list in Outlook 2007 with a CDO 1.2 script


Posted by Henning Krause on Thursday, May 22, 2008 2:36 PM, last modified on Thursday, May 22, 2008 2:39 PM
Permalink | Post RSSRSS comment feed

Getting Mailbox Information On Exchange 2003 using WMI

A recent question in the Microsoft development forum for Exchange was how to get information about mailboxes on Exchange 2003 from an ASP.NET application.

The simplest solution is to use WMI in this case. Exchange exposes certain mailbox statistics via the Exchange_Mailbox WMI class. The following code can be used to iterate through all mailboxes and print some of the properties:

   1: using System;
   2: using System.Management;
   3:  
   4: namespace MailboxSizer
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             var scope = new ManagementScope("\\\\.\\root\\MicrosoftExchangeV2");
  11:             var query = new ObjectQuery("select * from Exchange_Mailbox");
  12:             var searcher = new ManagementObjectSearcher(scope, query);
  13:             foreach (ManagementObject mailbox in searcher.Get())
  14:             {
  15:                 Console.WriteLine("Display name: {0}", mailbox["MailboxDisplayName"]);
  16:                 Console.WriteLine("Size: {0} bytes", mailbox["Size"]);
  17:                 Console.WriteLine("Storage Limit: {0}", (StorageLimitInfo) (uint) mailbox["StorageLimitInfo"]);
  18:  
  19:                 Console.WriteLine();
  20:             }
  21:             Console.ReadLine();
  22:         }
  23:     }
  24:  
  25:     [Flags]
  26:     internal enum StorageLimitInfo
  27:     {
  28:         BelowLimit = 1,
  29:         IssueWarning = 2,
  30:         ProhibitSend = 4,
  31:         NoChecking = 8,
  32:         MailboxDisabled = 16
  33:     }
  34: }

You need to reference the System.Management assembly for this code to work.


Posted by Henning Krause on Monday, May 19, 2008 8:00 PM, last modified on Tuesday, May 20, 2008 11:51 AM
Permalink | Post RSSRSS comment feed

ExOleDB problems on Exchange 2007

If you are using the ExOleDB provider on Exchange 2007 it might fail with this error message:

Error : Provider cannot be found. it may not be properly installed. / 800A0E7A / ADODB.Connection

I know of two errors which can cause this message:

  • Your are trying to access the ExOleDB provider from a 32bit process. Exchange 2007 is completely 64bit. This typically breaks old Visual Basic programs or scripts running under the 32bit version of the Windows Scripting Host. To check this, open the Task manager and search for the process name under the "Processes" tab. If the process name is appended with a *32, it runs as a 32bit process.
  • The COM registration of the ExOleDB provider is messed up. This can be fixed by calling Regsvr32 "C:\Program Files\Exchsrvr\bin\exodbprx.dll".

Posted by Henning Krause on Monday, May 19, 2008 4:19 PM, last modified on Tuesday, July 26, 2011 9:56 PM
Permalink | Post RSSRSS comment feed