InfiniTec - Henning Krauses Blog

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

Processing items in an Exchange folder using EWS Managed API

A requirement I often hear is something like this: “We have a mailbox that receives emails of some kind that need to be processed somehow”. What options are there to fulfill the requirement? As always, it depends. When the items need to be processed as soon as possible and the client processing the items can be directly reached by the Exchange server, push notifications are certainly a good choice. If, on the other hand, item processing is not time critical a suitable method of processing those items is polling. It’s much simpler to use than push notifications. A simple example might look like this:

   1: private static void ProcessItems(ExchangeService exchangeService)
   2: {
   3:     var offset = 0;
   4:     const int pageSize = 100;
   6:     FindItemsResults<Item> result;
   7:     do
   8:     {
   9:         var view = new ItemView(pageSize, offset);
   5:  
  10:  
  11:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  12:  
  13:         foreach (var item in result)
  14:         {
  15:             ProcessItem(item);
  16:         }
  17:         offset += pageSize;
  18:     } while (result.MoreAvailable);
  19: }

This is a very naïve implementation as it always returns every item from the inbox folder of the mailbox. But at least it uses paging. This breaks the processing down from one very large request into many small requests (in this case 100 items are returned per request).

This method is suitable if you delete the processed items from a mailbox after they are processed. Of course, the items should not be removed until the every item has been processed. Otherwise items may be skipped since the offsets of the individual items change when an item is removed from the folder.

If the items are not removed from the store, the calling application must distinguish new items from items already processed. An obvious way to do this is to mark each item once it has been processed by marking it as read. This changes the requirement for the ProcessItems method: It should only process unread items. This modifications has been incorporated into the following example:

   1: private static void ProcessItems(ExchangeService exchangeService)
   2: {
   3:     var offset = 0;
   4:     const int pageSize = 100;
   5:  
   6:     FindItemsResults<Item> result;
   7:     do
   8:     {
   9:         var view = new ItemView(pageSize, offset)
  10:         {
  11:             SearchFilter = new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false)
  12:         };
  13:  
  14:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  15:  
  16:         foreach (var item in result)
  17:         {
  18:             ProcessItem(item);
  19:         }
  20:         offset += pageSize;
  21:     } while (result.MoreAvailable);
  22: }

The only change in this example is in line 11: A search filter has been added to filter for unread items. What’s missing here is the modification of the Unread status of each message. And that is a considerable drawback of the whole solution. To touch each processed item, the UpdateItems method must be called. It is sufficient to call the UpdateItems method and pass all item ids from the last resultset to the method. But this adds a significant burden on the Exchange server and slows overall processing down. Furthermore, if someone accesses the mailbox (either with Outlook or Outlook Web Access) and accidently marks one or more items as read, those items will not be processed by the client application.

Next idea: Find all items that were received after the last time the mailbox was checked. A ProcessItems method that implements this behavior might look like this:

   1: private static void ProcessItems(ExchangeService exchangeService, DateTime lastCheck)
   2: {
   3:     var offset = 0;
   4:     const int pageSize = 100;
   5:  
   6:     FindItemsResults<Item> result;
   7:     do
   8:     {
   9:         var view = new ItemView(pageSize, offset)
  10:                        {
  11:                            SearchFilter = new SearchFilter.IsGreaterThanOrEqualTo(ItemSchema.DateTimeReceived, lastCheck)
  12:                        };
  13:  
  14:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  15:  
  16:         foreach (var item in result)
  17:         {
  18:             ProcessItem(item);
  19:         }
  20:         offset += pageSize;
  21:     } while (result.MoreAvailable);
  22: }

As with the second example, the difference to the first one is line 11. A search filter has been added that restricts the FindItems call to those items received after a specific time. This method removes the requirement to mark each processed item on the server. But it adds another caveat: A mail that is received during a ProcessItems call will be missed if no additional checks are performed. This can be a little tricky.

Luckily, the Exchange WebServices offer a more suitable solution for the whole requirement: The SyncFolderItems method. This method not only solves the problems mentioned above but also returns deleted items, if such a processing is necessary. A ProcessItems method that uses this API now looks like this:

   1: private static string SyncItems(ExchangeService exchangeService, string syncState)
   2: {
   3:     const int pageSize = 100;
   4:  
   5:     ChangeCollection<ItemChange> changeCollection;
   6:     do
   7:     {
   8:         changeCollection = exchangeService.SyncFolderItems(new FolderId(WellKnownFolderName.Inbox),
   9:                                                            new PropertySet(BasePropertySet.FirstClassProperties), null, pageSize,
  10:                                                            SyncFolderItemsScope.NormalItems, syncState);
  11:  
  12:         foreach (var change in changeCollection)
  13:         {
  14:             if (change.ChangeType == ChangeType.Create)
  15:             {
  16:                 ProcessItem(change.Item);
  17:             }
  18:         }
  19:         syncState = changeCollection.SyncState;
  20:  
  21:     } while (changeCollection.MoreChangesAvailable);
  22:     return syncState;
  23: }

This method only processes newly created items and ignores all other item changes or deletions. The application calling this method needs to store the synchronization state between each call. If an empty sycnState is provided, Exchange will return every item from the mailbox as “Created” item. This makes it possible to process all existing items once and then, with the same logic, every changed item. The only drawback with this method is the fact that the synchronization state can become quite big (in a folder with ~4500 items, the syncstate has a size of approx. 60kb).


Posted by Henning Krause on Sunday, June 7, 2009 12:54 PM, last modified on Monday, November 29, 2010 9:21 PM
Permalink | Post RSSRSS comment feed

Getting the fullqualified DNS name of the current computer

Under certain circumstances a program needs to determine the name of the computer it’s running on. The first approach to get this name is to use the System.Environment.MachineName property. However, this name only reflects the NETBIOS name of the current machine. But in larger environments a full-qualified name including the DNS domain the computer belongs to. This can be something like computername.contoso.local. One example where this full qualified name might be needed are Exchange Push notification. I’ve published a component to CodePlex makes it really easy to incorporate them in an application. However, for the notifications to reach the client the component needs to tell the Exchange server a correct callback address. In a very simple network environment, it is sufficient to specify the NETBIOS hostname. But in more complex environments, Exchange might not be able to send a notification because it cannot correctly resolve the unqualified hostname to an IP address.

The full qualified domain name of the current host can be resolved with a call tot the System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties method. This method returns, among other things, the required information:

   1: var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
   2: string fullQualifiedDomainName;
   3:  
   4: if (!string.IsNullOrEmpty(ipGlobalProperties.DomainName))
   5: {
   6:     fullQualifiedDomainName = string.Format("{0}.{1}", ipGlobalProperties.HostName, ipGlobalProperties.DomainName);
   7: }
   8: else
   9: {
  10:     fullQualifiedDomainName = ipGlobalProperties.HostName;
  11: }

I have updated the PushNotification component to reflect this new behavior.


Posted by Henning Krause on Saturday, June 6, 2009 6:23 PM, last modified on Saturday, June 6, 2009 11:39 PM
Permalink | Post RSSRSS comment feed

InfiniTec.Exchange.Notifications updated to 1.5.0.0

I have just published a new version of the my notification component on CodePlex. The new version has some breaking changes to the previous version but it should be simpler to use. Additionally, I have added a small sample application that shows how to use the component. It’s a small WPF application that allows a user to subscribe to calendars of multiple users. The application uses the new Managed API which made the whole thing a whole lot easier to write. Here is a class diagram of the component:

ClassDiagram

If you have worked with the component before you’ll notice that the Subscription class has lost some of its members. I have removed those methods to avoid confusion on how the component is utilized. The only way to create a new subscription now is to use the SubscriptionCollection class. Here is an example on how to use the component:

   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:             using (var adapter = new ExchangeWebServicesAdapter(new Uri("https://w2k3x64/ews/exchange.asmx"), 
  21:                 new NetworkCredential("administrator", "password", "contoso")))
  22:             using (var listener = new PushNotificationListener())
  23:             {
  24:                 Console.Out.WriteLine("Starting Notification Service...");
  25:                 listener.Start();
  26:  
  27:                 SubscriptionCollection subscriptionCollection;
  28:  
  29:                 if (File.Exists(StateSaverFilename))
  30:                 {
  31:                     using (var stream = File.OpenRead(StateSaverFilename))
  32:                     {
  33:                         subscriptionCollection = SubscriptionCollection.Deserialize(stream, adapter, listener);
  34:                         subscriptionCollection.SubscriptionRestartCompleted +=
  35:                             SubscriptionCollection_OnSubscriptionRestartCompleted;
  36:                         subscriptionCollection.SubscriptionRestartProgressChanged +=
  37:                             SubscriptionCollection_OnSubscriptionRestartProgressChanged;
  38:  
  39:                         subscriptionCollection.RestartAsync();
  40:                     }
  41:                 }
  42:                 else
  43:                 {
  44:                     // Create a new subscription collection to manage all the subscriptions  
  45:                     // Register for a NewMail notification on the inbox of the administrator
  46:                     subscriptionCollection = new SubscriptionCollection(adapter, listener)
  47:                                                  {
  48:                                                      {new[] {new FolderReference(WellKnownFolderId.Inbox)}, EventTypes.All}
  49:                                                  };
  50:                 }
  51:  
  52:                 Console.Out.WriteLine("Creating subscription");
  53:                 foreach (var subscription in subscriptionCollection)
  54:                 {
  55:                     // Write a line to the console for each new mail received  38:  
  56:                     subscription.NewMail += (sender, e) =>
  57:                                             Console.Out.WriteLine(string.Format("{0}: New Mail arrived in your inbox", e.Timestamp));
  58:                     subscription.Start();
  59:                 }
  60:                 Console.Out.WriteLine("Waiting for notifications... Hit [Enter] to quit...");
  61:                 Console.ReadLine();
  62:  
  63:                 Console.Out.WriteLine("Saving the current state of the notification listener...");
  64:                 using (var stream = File.OpenWrite(StateSaverFilename))
  65:                 {
  66:                     subscriptionCollection.Serialize(stream);
  67:                 }
  68:                 Console.Out.WriteLine("State saved to {0}", Path.GetFullPath(StateSaverFilename));
  69:             }
  70:         }
  71:  
  72:  
  73:         private static void SubscriptionCollection_OnSubscriptionRestartProgressChanged(object sender, ProgressChangedEventArgs args)
  74:         {
  75:             Console.Out.WriteLine("Subscription restart {0}% complete.", args.ProgressPercentage);
  76:         }
  77:  
  78:         private static void SubscriptionCollection_OnSubscriptionRestartCompleted(object sender, AsyncCompletedEventArgs<SubscriptionRestartErrorSummary> args)
  79:         {
  80:             Console.Out.WriteLine("Subscription restart is complete. {0} subscriptions could not be restarted.", args.Result.Errors.Count);
  81:         }
  82:     }
  83: }

Posted by Henning Krause on Saturday, May 23, 2009 12:38 PM, last modified on Saturday, May 23, 2009 12:40 PM
Permalink | Post RSSRSS comment feed

Searching a meeting with a specific UID using Exchange Web Services 2007

For most items, the item id (which is based on the MAPI entry id) is a good candidate for a long-term identifier. As long as the item is not moved between folders, it remains stable. At least for most items. This is, however, not true for meetings. Exchange recreates meeting items under certain circumstances and the meeting items loose their original entry id, thus the item id is also modified. But a meeting has a different property which uniquely identifies it, even across mailbox boundaries: The UID property. This property is available as a first-class property since Exchange 2007 Service Pack 1. It is a string that looks like this:

   1: 040000008200E00074C5B7101A82E008000000007069352667BBC9010000000000000000100000000C74ABD802575A41BC09B0E12352657B

Since this property is not an Item Id, the GetItem operation cannot be used to bind to a meeting element directly. Sadly, the FindItem operation doesn’t work either, because the Exchange WebServices do not accept a restriction that is based on the UID property.

Luckily, the UID can be read and searched via a MAPI property: The GlobalObjectId. This is a binary property and when fetched looks like this:

   1: BAAAAIIA4AB0xbcQGoLgCAAAAABwaTUmZ7vJAQAAAAAAAAAAEAAAAAx0q9gCV1pBvAmw4SNSZXs=

The value is the equivalent to the UID displayed above, but in a different format. The UID is formatted as a hex-string and the GlobalObjectId is displayes as a Base64 string. Using the following small function, the first representation can be converted into the latter:

   1: private static string GetObjectIdStringFromUid(string id)
   2: {
   3:     var buffer = new byte[id.Length/2];
   4:     for (int i = 0; i < id.Length/2; i++)
   5:     {
   6:         var hexValue = byte.Parse(id.Substring(i*2, 2), NumberStyles.AllowHexSpecifier);
   7:         buffer[i] = hexValue;
   8:     }
   9:     return Convert.ToBase64String(buffer);
  10: }

The value returned from the GetObjectIdStringFromUid can now be used to execute a FindItem operation, specifying a restriction on the GlobalObjectId property. Here is the whole function:

   1: static void Main(string[] args)
   2: {
   3:     ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
   4:  
   5:     const string id = "040000008200E00074C5B7101A82E008000000007069352667BBC9010000000000000000100000000C74ABD802575A41BC09B0E12352657B";
   6:  
   7:     string bufferString = GetObjectIdStringFromUid(id);
   8:  
   9:     var service = new ExchangeServiceBinding
  10:         {
  11:           Url = "https://w2k3x64/ews/exchange.asmx",
  12:           Credentials = new NetworkCredential("bob", "Password!", "contoso"),
  13:           RequestServerVersionValue = new RequestServerVersion { Version = ExchangeVersionType.Exchange2007_SP1}
  14:         };
  15:  
  16:     var response = service.FindItem(new FindItemType
  17:         {
  18:             ParentFolderIds = new[] { new DistinguishedFolderIdType { Id = DistinguishedFolderIdNameType.calendar } },
  19:             ItemShape = new ItemResponseShapeType
  20:                 {
  21:                     BaseShape = DefaultShapeNamesType.AllProperties,
  22:                 },
  23:             Restriction = new RestrictionType {
  24:                 Item = new IsEqualToType
  25:                     {
  26:                       Item = new PathToExtendedFieldType
  27:                         {
  28:                            DistinguishedPropertySetId = DistinguishedPropertySetType.Meeting,
  29:                            DistinguishedPropertySetIdSpecified = true,
  30:                            PropertyId = 0x03,
  31:                            PropertyIdSpecified = true,
  32:                            PropertyType = MapiPropertyTypeType.Binary
  33:                         },
  34:                       FieldURIOrConstant = new FieldURIOrConstantType
  35:                         {
  36:                           Item = new ConstantValueType
  37:                           {
  38:                               Value = bufferString
  39:                           }
  40:                         }
  41:                     }
  42:                 }
  43:         });
  44:     var messageType = ((FindItemResponseMessageType) response.ResponseMessages.Items[0]);
  45:     Console.Out.WriteLine("messageType.ResponseClass = {0}", messageType.ResponseClass);
  46:  
  47:     if (messageType.ResponseClass != ResponseClassType.Success)
  48:     {
  49:         Console.Out.WriteLine(messageType.MessageText);
  50:         return;
  51:     }
  52:     var returnedItems = messageType.RootFolder.Item as ArrayOfRealItemsType;
  53:  
  54:     if (returnedItems == null || returnedItems.Items == null)
  55:     {
  56:         Console.Out.WriteLine("Nothing found.");
  57:         return;
  58:     }
  59:  
  60:     foreach (CalendarItemType item in returnedItems.Items)
  61:     {
  62:         Console.Out.WriteLine("item.Subject = {0}", item.Subject);
  63:         Console.Out.WriteLine("item.UID = {0}", item.UID);
  64:     }
  65:     Console.ReadLine();
  66: }

Posted by Henning Krause on Monday, April 13, 2009 11:45 PM, last modified on Sunday, May 8, 2011 8:37 AM
Permalink | Post RSSRSS comment feed

Resolving the primary email address with Exchange WebServices ResolveNames operation

To access another users mailbox with Exchange WebServices, the primary email address of that mailbox is required. Secondary email addresses just don’t work. But if that all one’s got, the secondary email address needs to be resolved to the primary email address. The ResolveNames operation seems a good candidate for that job. But feeding that operation with a secondary email address will yield no results.

The ResolveNames operation executes an ANR search (Ambiguous name resolution) request on Active Directory, which translates (at least in part) to: (|(…)(proxyAddresses=address*)(…)). But the syntax of the proxyAddresses field is different: It is a multi-valued field where each entry has the form protocol:address. For normal email addresses, an entry looks like this: smtp:jdoe@contoso.com.

To resolve a secondary email address, the ResolveNames operation has to be called with something like smtp:jdoe@contoso.com.


Posted by Henning Krause on Monday, April 13, 2009 2:26 PM, last modified on Sunday, November 28, 2010 3:38 AM
Permalink | Post RSSRSS comment feed

Saving custom data on Exchange elements with The Exchange WebServices

The database used by Exchange is quite a versatile one. A developer can save custom data on each item by creating additional properties.

Generally, there are two types of identification for a property: Either, a property id used (for example PR_ENTRYID which has an id of 0x0fff) or a name. Additionally, some properties live in so called property sets, like most of the task fields. There are some predefined property sets, but a developer can choose to create his own. Since property sets are identified by a GUID, it is rather unlikely that two developers select a property set which collide with each other. In addition to the property name, a property also has a data type. The valid types are defined in the MapiPropertyTypeType enumeration.

One property set is of particular interest, namely the one called PublicStrings. All custom properties created with Outlook are stored in this set. If a custom property is designed to be used by custom Outlook formulas, the developer must choose this property set. In any other case, it is better to create a random GUID and use that property set to prevent collisions with other applications.

So, how are these properties accessed? The answer can be found in the PathToExtendedFieldType. The following code reads the custom Outlook property “CustomOutlookProperty”, which has a data type of string from a given item:

   1: public void GetRequest(ItemIdType itemId)
   2: {
   3:     using (var binding = CreateEwsBinding())
   4:     {
   5:         var customOutlookPropertyPath = new PathToExtendedFieldType
   6:             {
   7:                 DistinguishedPropertySetId = DistinguishedPropertySetType.PublicStrings,
   8:                 DistinguishedPropertySetIdSpecified = true,
   9:                 PropertyName = "CustomOutlookProperty",
  10:                 PropertyType = MapiPropertyTypeType.String
  11:             };
  12:         var request = new GetItemType
  13:               {
  14:                   ItemIds = new[] {itemId},
  15:                   ItemShape = new ItemResponseShapeType
  16:                     {
  17:                         AdditionalProperties = new[]
  18:                                {
  19:                                    customOutlookPropertyPath
  20:                                }
  21:                     }
  22:               };
  23:  
  24:         var response = binding.GetItem(request);
  25:  
  26:         // Need to check response for errors. Ommited for clarity
  27:  
  28:         var item = ((ItemInfoResponseMessageType) response.ResponseMessages.Items[0]).Items.Items[0];
  29:         var customOutlookPropertyValue =
  30:             (from extendedProperty in item.ExtendedProperty
  31:              where
  32:                  extendedProperty.ExtendedFieldURI.DistinguishedPropertySetId == DistinguishedPropertySetType.PublicStrings &&
  33:                  extendedProperty.ExtendedFieldURI.PropertyName == "CustomOutlookProperty"
  34:              select (string) extendedProperty.Item).FirstOrDefault();
  35:     }
  36: }

The important thing happens in lines 5 through 11. Theses lines define the path to the custom outlook property.

If you want to access a custom property that should not be directly available via Outlook, create a custom GUID for your application. For this example, I use this code:

   1: private static readonly Guid PrivatePropertySetId = new Guid("9C24B417-DDC1-4F5F-974D-E35FCF6E9FE2");

Then, replace the lines 5 through 11 with this code:

   1: var customOutlookPropertyPath = new PathToExtendedFieldType
   2:     {
   3:         PropertySetId = PrivatePropertySetId.ToString(),
   4:         PropertyName = "CustomProperty",
   5:         PropertyType = MapiPropertyTypeType.String
   6:     };

To extract the property from the response, use this snippet:

   1: var item = ((ItemInfoResponseMessageType) response.ResponseMessages.Items[0]).Items.Items[0];
   2: var customPropertyValue =
   3:     (from extendedProperty in item.ExtendedProperty
   4:      where
   5:         PrivatePropertySetId.ToString().Equals(extendedProperty.ExtendedFieldURI.PropertySetId, StringComparison.OrdinalIgnoreCase) &&
   6:          extendedProperty.ExtendedFieldURI.PropertyName == "CustomProperty"))
   7:      select (string) extendedProperty.Item).FirstOrDefault();

Note, that I used the the Equals method along with the comparison mode OrdinalIgnoreCase to compare the two property set ids.


Posted by Henning Krause on Friday, March 27, 2009 6:12 PM, last modified on Monday, November 29, 2010 8:30 PM
Permalink | Post RSSRSS comment feed

Custom OWA Forms in Exchange 2007 – Refreshing OWA after editing a custom element

This is some sort of follow up to yesterdays post about custom OWA forms. If a user opens an OWA form for editing, OWA opens the edit form for the item in a new window. In most forms, a click on the “Save” button will also close this window and OWA will reload the preview window as well as the items list via a custom AJAX call. Unfortunately, Microsoft has not documented how to do this for custom forms, so the method described here is to be considered totally unsupported, and it may stop to work with any future service pack or roll-up.

Two javascript functions have to be called to execute a refresh of both the preview pane and the items list:

   1: window.opener.ref();      // refresh the list view
   2: window.opener.udRP(1);    // refresh the reading pane
   3: window.close();           // close the edit window

If you followed my steps from the last post, you can use a standard button (or link button) to create your own save button. In that case, use the following code to emit the required script:

   1: protected void SaveButton_Click(object sender, EventArgs e)
   2: {
   3:     if (!Page.IsValid)
   4:     {
   5:         return;
   6:     }
   7:  
   8:     // Do what is necessary to save the changes made by the user
   9:         
  10:     Response.ClearContent();
  11:     Response.Write("<script language='javascript'>window.opener.ref(); window.opener.udRP(1); window.close();</script>");
  12:     Response.End();
  13: }

Posted by Henning Krause on Tuesday, March 24, 2009 10:17 PM, last modified on Monday, November 29, 2010 6:49 PM
Permalink | Post RSSRSS comment feed

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