InfiniTec - Henning Krauses Blog

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

Custom OWA Forms in Exchange 2007 – Reenabling standard postback functionality

With Service Pack 1, Microsoft introduced some features with Outlook Web Access that allows a developer to create custom forms. These custom forms can be used to display and edit items with custom message classes, much like Outlook does. Details of these features can be found on MSDN here and here and fellow MVP Glen Scales has an example here. Basically, all one needs to do is creating some formulas (in the form of .aspx files) and a registry.xml, describing the mapping of IPM message classes (say IPM.Note.Custom) to these formulas. Simple enough. And since aspx files are easy to develop, it should be a piece of cake to create a full-fledged solution.

So, here is the catch – To retain a single-sign on experience, all the forms have to be put in a directory beneath the directory C:\Program Files\Microsoft\Exchange Server\ClientAccess\Owa\forms. Then, a web-application has to be created that runs in the default OWA application pool (MSExchangeOWAAppPool). This way, OWA will handle all the authentication stuff and you can use it to call Exchange WebServices. The problem with this approach is this: OWA uses an HttpModule (Microsoft.Exchange.Clients.Owa.Core.OwaModule, implemented in the assembly Microsoft.Exchange.Clients.Owa.dll) that inspects every request that is made to a file beneath the /owa virtual directory. At least, every file that is controlled by the ASP.NET runtime. Any direct request will be rejected. This leads to two problems:

  1. When the custom form is rendered in the client browsers, the forms action attribute will point to something like “/owa/forms/customform/editform.aspx”. Any attempt to post back to this location will fail.
  2. ASP.NET uses a javascript to execute the post back. Even just a standard HTML button is used. But part of the required javascript is loaded from an external script. And since .NET 2.0, a handler called WebResource.axd is used to serve this script. Again, OWA will block any attempt to load this script.

The solution to the first problem is quite simple: ASP.NET allows the modification of the form tag from a code-behind file:

   1: protected override void OnLoad(EventArgs e)
   2: {
   3:     base.OnLoad(e);
   4:     if (Form.Action.IsNullOrEmpty())
   5:     {
   6:         Form.Action = "/owa/" + Request.Url.Query;
   7:     }
   8:     Response.Cache.SetExpires(DateTime.Now);
   9:     Response.Cache.SetLastModified(DateTime.Now);
  10: }

The important part is implemented in lines 4 to 7. This adjusts the action attribute of the form element to point to a direction OWA accepts. The lines 8 and 9 ensure that the form is always executed again. When omitted, OWA will gladly cache the files for quite some time – even if the underlying element changes.

The second problem does not have such an elegant solution. In fact, it now gets quite messy. Since OWA blocks access to the WebResource.axd handler, the only solution I found is to create a new web application project, start it up and manually download the script referenced in the standard aspx file. Once done, give the script a frindly name (something like DefaultScript.js) and put it in the folder of your custom forms and reference the script file from within your custom form with a <script> tag:

   1: <script language="javascript" src="/owa/forms/crmproject/DefaultScript.js"></script>
Far from nice, but it works.

Posted by Henning Krause on Monday, March 23, 2009 10:05 PM, last modified on Monday, November 29, 2010 7:18 AM
Permalink | Post RSSRSS comment feed

Setting the Task owner on new items using Exchange WebServices

If you try to set the owner of a task using Exchange WebServices, the server will tell you that this is not allowed. To circumvent this restriction, you’ll have to set the MAPI property directly. This is done by creating an ExtendedPropertyType instance and adding that to an UpdateItemType.

   1: var modification = new PathToExtendedFieldType
   2:     {
   3:         DistinguishedPropertySetId = DistinguishedPropertySetType.Task,
   4:         DistinguishedPropertySetIdSpecified = true,
   5:         PropertyId = 0x811f,
   6:         PropertyIdSpecified = true,
   7:         PropertyType = MapiPropertyTypeType.String
   8:     }, value);

However, this won’t work when creating new items. Exchange will return an error complaining about an invalid PropertySet id. The only solution I found is to issue a CreateItem request first. On this request, the task owner is omitted. After the item has been created, use the ItemId from the CreateItem response and execute an UpdateItem request setting only the task owner.


Posted by Henning Krause on Thursday, March 19, 2009 7:36 PM, last modified on Monday, November 29, 2010 9:31 PM
Permalink | Post RSSRSS comment feed

Push-Notifications – Surviving application restarts

This is the fourth article about the .NET component I published at CodePlex recently. To all related articles, click here.

If you subscribe to a folder on an Exchange mailbox or public folder, the server will try to send notifications to the registered endpoint of yours upon every modification. If your application is shutdown, Exchange will try to reach it for a certain amount of time. Once this time span has elapsed, it will delete the subscription. If the application is restarted and a new subscription is created, it will be notified about all subsequent events. But for certain use cases, it is imperative that no events are missed – even events which occurred between application downtime must be catched. One example for this is a synchronization applications which keeps a sql server database (like a CRM application) and Exchange folders in sync.

For these types of scenarios, all Exchange events carry a watermark with them. This watermark is opaque to the client, but for the Exchange server it contains enough information to reconstruct changes made since the watermark was received. To use this features, an application has to save each watermark it gets. The watermark can be sent to the Exchange server along with the subscription request for a folder. Exchange will then replay the events which happened since that watermark has been generated.

I’ve implemented this feature in my Push Notification component in the SubscriptionCollection class. It can be serialized to either a System.IO.Stream or System.Xml.XmlWriter. The latter one uses the System.Runtime.Serialization.DataContractSerializer to perform the serialization. And the way it is used, it requires .NET Framework 3.5 Service Pack 1. If you cannot rely on having this version installed on your clients machines, you should probably use the binary serialization instead.

Here is a small code sample on how to use the serialization feature (this is an example only, so the exception handling is far from optimal…)

   1: using System;
   2: using System.ComponentModel;
   3: using System.IO;
   4: using System.Net;
   5: using InfiniTec.Exchange.Notifications;
   6: using InfiniTec.Threading;
   7:  
   8: namespace NewMailNotificationExample
   9: {
  10:     internal class Program
  11:     {
  12:         private const string _StateSaverFilename = "notificationstate.bin";
  13:  
  14:         private static void Main()
  15:         {
  16:             // Ignore any certificate errors
  17:             ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
  18:             
  19:             // Setup the adapter which will be used to call into the Exchange WebService  
  20:             var adapter = new ExchangeWebServicesAdapter(new Uri("https://w2k3x64/ews/exchange.asmx"),
  21:                                                          new NetworkCredential("administrator", "password", "contoso"));
  22:  
  23:  
  24:             // Setup a listener that listens on port 80 on the local computer          
  25:  
  26:             using (var listener = new PushNotificationListener())
  27:             {
  28:                 Console.Out.WriteLine("Starting Notification Service...");
  29:                 listener.Start();
  30:  
  31:                 SubscriptionCollection subscriptionCollection;
  32:  
  33:                 if (File.Exists(_StateSaverFilename))
  34:                 {
  35:                     using (var stream = File.OpenRead(_StateSaverFilename))
  36:                     {
  37:                         subscriptionCollection = SubscriptionCollection.Deserialize(stream);
  38:                         subscriptionCollection.SubscriptionRestartCompleted += 
  39:                             SubscriptionCollection_OnSubscriptionRestartCompleted;
  40:                         subscriptionCollection.SubscriptionRestartProgressChanged +=
  41:                             SubscriptionCollection_OnSubscriptionRestartProgressChanged;
  42:  
  43:                         subscriptionCollection.RestartAsync(listener, adapter);
  44:                     }
  45:                 }
  46:                 else
  47:                 {
  48:                     // Create a new subscription collection to manage all the subscriptions  
  49:                     // Register for a NewMail notification on the inbox of the administrator
  50:                     subscriptionCollection = new SubscriptionCollection(adapter)
  51:                                                  {
  52:                                                      {new[] {new FolderReference(WellKnownFolderId.Inbox)}, EventTypes.NewMail}
  53:                                                  };
  54:                 }
  55:  
  56:                 Console.Out.WriteLine("Creating subscription");
  57:                 foreach (var subscription in subscriptionCollection)
  58:                 {
  59:                     // Write a line to the console for each new mail received  38:  
  60:                     subscription.NewMail += (sender, e) =>
  61:                                             Console.Out.WriteLine(string.Format("{0}: New Mail arrived in your inbox", e.Timestamp));
  62:                     subscription.Start(listener);
  63:                 }
  64:                 Console.Out.WriteLine("Waiting for notifications... Hit [Enter] to quit...");
  65:                 Console.ReadLine();
  66:  
  67:                 Console.Out.WriteLine("Saving the current state of the notification listener...");
  68:                 using (var stream = File.OpenWrite(_StateSaverFilename))
  69:                 {
  70:                     subscriptionCollection.Serialize(stream);
  71:                 }
  72:                 Console.Out.WriteLine("State saved to {0}", Path.GetFullPath(_StateSaverFilename));
  73:             }
  74:         }
  75:  
  76:         private static void SubscriptionCollection_OnSubscriptionRestartProgressChanged(object sender, ProgressChangedEventArgs args)
  77:         {
  78:             Console.Out.WriteLine("Subscription restart {0}% complete.", args.ProgressPercentage);
  79:         }
  80:  
  81:         private static void SubscriptionCollection_OnSubscriptionRestartCompleted(object sender, AsyncCompletedEventArgs<SubscriptionRestartErrorSummary> args)
  82:         {
  83:             Console.Out.WriteLine("Subscription restart is complete. {0} subscriptions could not be restarted.", args.Result.Errors.Count);
  84:         }
  85:     }
  86: }

 

This essentially is the sample from my first blog post in this series, but enhanced to take advantages of the restart capabilities of the SubscriptionCollection class. The program checks to see if there is a saved state from a previous run available. If found, it is deserialized and restarted (lines 35 to 43). Otherwise a new instance is created. Finally, the events are wired to the individual subscriptions (line 57 to 62). And once the application stops, the current state is serialized to a file.

Since the subscriptions are restarted asynchronously in the background, the SubscriptionRestartCompleted and SubscriptionRestartProgressChanged events are used to keep track of the deserialization process. The progress changed event handler is raised for each subscription restarted. Once complete, the SubscriptionRestartCompleted handler is raise. The event args this event handler contain information about all failed restart attempts.


Posted by Henning Krause on Monday, January 12, 2009 11:36 PM, last modified on Monday, January 12, 2009 11:36 PM
Permalink | Post RSSRSS comment feed

Push notifications with WCF – Security considerations

This is the third article about the .NET component I published at CodePlex recently. To all related articles, click here.

The PushNotificationListener I created for that component uses a WCF endpoint to receive notifications. This means that it needs to open an TCP endpoint of some sort. Thanks to WCF, all the hard stuff is done by the WCF infrastructure. There are, however, two issues which must be resolved before you can receive notifications from your Exchange Server: The Windows Firewall, which blocks all incoming traffic by default and the WCF permission system. To deal with the former, you need to setup an exception for a specific port or for your executable. If you are using Windows Installer Xml, you can use the Firewall Extension to create such an exception during the setup of your application. If you want to do this directly from your application, you will need administrative rights and perform some interop stuff (see the links at the end of the article).

To open an WCF endpoint, you need administrative permissions too, by default. Because this is a very nasty requirement, the system allows you to create so-called reservations which can be tied to specific users. You can use nethsh.exe to manipulate these permissions. You can also use the httpcfg.exe tool. Or, you save you the headache that comes with these tools and head over to Paul Wheelers blog and take a look at this blog post of him: AddressAccessDeniedException: HTTP could not register URL http://+:8080/<…>. He has published a small tool (full source included) to enumerate and manage WCF port reservations.

Here is a screenshot of the reservations on my machine:

image

The really only real interesting endpoint here is the second from the bottom: http://+:80/Temporary_Listen_Addresses. This is a reservation in the form of an UrlPrefix String (more about that here on MSDN) that can be used by everyone (and Dominick Baier on leastprivilege.com has a nice post about the security ramifications here). However, the reservation is there and we can use it for the PushNotificationListener. This is the reason why the default port is 80 and the RelativePath property is set to a folder below the Temporary_Listen_Addresses. If you need to use another relative path or even another port, you’ll have to create a reservation for it. You can take a look at the source code of the HttpNamespaceManager tool from Paul Wheelers Blog on how to do this. Again, this is probably best done during setup. Again, if you are using Windows Installer Xml, you can use a managed custom action (using the DTF Framework) to create the necessary reservation during the setup of your application.

Controlling the Windows Firewall with C#

Here are a few links I just found on the internet. I have tested none of them, but they might give you a hint:

http://www.shafqatahmed.com/2008/01/controlling-win.html

http://www.codeproject.com/KB/winsdk/WinXPSP2Firewall.aspx


Posted by Henning Krause on Thursday, January 1, 2009 9:24 PM, last modified on Thursday, January 1, 2009 9:24 PM
Permalink | Post RSSRSS comment feed

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