InfiniTec - Henning Krauses Blog

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

Using Streaming Notifications with Exchange 2010

That is the title of an article I wrote for Microsoft. It has been published here a few days ago:

Summary: Streaming notifications, which combine the functionalities of push and pull notifications, are new in Microsoft Exchange Server 2010 Service Pack 1 (SP1). This article explains how streaming notifications work and when to use them.

The code sample is missing, but that will be fixed in a few days.


Posted by Henning Krause on Friday, July 15, 2011 7:50 PM, last modified on Saturday, July 30, 2011 10:38 PM
Permalink | Post RSSRSS comment feed

Enumerating Sharepoint Connections in a mailbox with EWS

An interesting question on StackOverflow came up recently: Is it possible to get the SharePoint lists which are connected to an Exchange Mailbox? The data which is synchronized with Outlook is stored in a PST file on the local disk – so no interaction with Exchange on this end. But if a user logs on from another computer, the SharePoint list the user has subscribed to are synchronized there as well. So the configuration seems to be stored in the mailbox somewhere. And indeed, they are. Outlook creates a message item in the associated folder table in the users inbox. Associated items are not visible from Outlook, but they can be accessed using MAPI or EWS. It turns out that Outlook saves the SharePoint connections similar to the RSS feeds. So a good starting point is to have a look at the Sharing Message Object Protocol Specification which lists the properties used for these items.

The SharePoint configuration items have a MessageClass of IPM.Sharing.Index.In, and the property PidLidSharingProviderGuidProperty is set to {0006F0AD-0000-0000-C000-000000000046}.

The configuration data is stored on a few properties. The following method lists all SharePoint connections connected to a mailbox:

   1:  using System;
   2:  using System.Net;
   3:  using Microsoft.Exchange.WebServices.Data;
   4:   
   5:  namespace ExchangeTest
   6:  {
   7:      internal class Program
   8:      {
   9:          private static readonly Guid PropertySetSharing = new Guid("{00062040-0000-0000-C000-000000000046}");
  10:   
  11:          private static readonly ExtendedPropertyDefinition PidLidSharingProviderGuidProperty =
  12:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A01, MapiPropertyType.CLSID);
  13:   
  14:          private static readonly ExtendedPropertyDefinition SharingRemotePathProperty =
  15:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A04, MapiPropertyType.String);
  16:   
  17:          private static readonly ExtendedPropertyDefinition SharingLocalNameProperty =
  18:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A0F, MapiPropertyType.String);
  19:   
  20:          private static readonly ExtendedPropertyDefinition SharingRemoteNameProperty =
  21:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A05, MapiPropertyType.String);
  22:   
  23:          private static readonly ExtendedPropertyDefinition SharingBrowseUrlProperty =
  24:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A51, MapiPropertyType.String);
  25:   
  26:          private static readonly ExtendedPropertyDefinition SharingRemoteTypeProperty =
  27:              new ExtendedPropertyDefinition(PropertySetSharing, 0x8A1D, MapiPropertyType.String);
  28:   
  29:          private static readonly Guid SharePointProviderId = new Guid("{0006F0AD-0000-0000-C000-000000000046}");
  30:   
  31:          public static void Main(string[] args)
  32:          {
  33:              var service = new ExchangeService(ExchangeVersion.Exchange2010)
  34:                            {Credentials = new NetworkCredential(test@contoso.com, "Password!")};
  35:              
  36:              service.AutodiscoverUrl(test@contoso.com, url => true);
  37:   
  38:              var folder = Folder.Bind(service, WellKnownFolderName.Inbox);
  39:              var filter = new SearchFilter.SearchFilterCollection(LogicalOperator.And,
  40:                                                                   new SearchFilter.IsEqualTo(ItemSchema.ItemClass,
  41:                                                                                              "IPM.Sharing.Index.In"),
  42:                                                                   new SearchFilter.IsEqualTo(PidLidSharingProviderGuidProperty,
  43:                                                                                              SharePointProviderId.ToString()));
  44:              var view = new ItemView(512)
  45:                         {
  46:                             Traversal = ItemTraversal.Associated,
  47:                             PropertySet = new PropertySet(BasePropertySet.IdOnly,
  48:                                                           SharingRemotePathProperty, SharingBrowseUrlProperty,
  49:                                                           SharingLocalNameProperty, SharingRemoteNameProperty,
  50:                                                           SharingRemoteTypeProperty)
  51:                         };
  52:   
  53:              var items = folder.FindItems(filter, view);
  54:              foreach (var item in items)
  55:              {
  56:                  Console.Out.WriteLine("RemotePath = {0}", item.GetValueOrDefault<string>(SharingRemotePathProperty));
  57:                  Console.Out.WriteLine("BrowseUrl = {0}", item.GetValueOrDefault<string>(SharingBrowseUrlProperty));
  58:                  Console.Out.WriteLine("LocalName = {0}", item.GetValueOrDefault<string>(SharingLocalNameProperty));
  59:                  Console.Out.WriteLine("Remotename = {0}", item.GetValueOrDefault<string>(SharingLocalNameProperty));
  60:                  Console.Out.WriteLine("Type = {0}", item.GetValueOrDefault<string>(SharingRemoteTypeProperty));
  61:                  Console.Out.WriteLine(new string('=', 80));
  62:              }
  63:          }
  64:      }
  65:   
  66:      public static class ItemExtension
  67:      {
  68:          public static T GetValueOrDefault<T>(this Item item, PropertyDefinitionBase property, T defaultValue = default(T))
  69:          {
  70:              T result;
  71:              return item.TryGetProperty(property, out result) ? result : defaultValue;
  72:          }
  73:      }
  74: }

This method dumps the configuration of all SharePoint connections to the console.

To use this method, you’ll need .NET 4. If you are running .NET 2.0, you’ll have to adapt it.

Additionally, this won't work with Exchange 2007, because EWS in that version does not allow a FindItems call on the associated items table. WebDAV is the API of choice in this case.


Posted by Henning Krause on Wednesday, July 13, 2011 9:05 PM, last modified on Wednesday, July 13, 2011 9:05 PM
Permalink | Post RSSRSS comment feed

Correlating a Non-Delivery-Notification to its original message with the Exchange Managed API

If you are automatically process emails in an Exchange Mailbox, you might also need to process non delivery reports (NDRs) and correlate these NDRs to their original values. Luckily, Exchange does most of the heavy lifting, as long as the NDR conforms to the RFC 3461, which defines the proper structure for an NDR that can be automatically parsed. Not all MTAs (Mail transfer agents) do generate these types of NDRs.

First of all, you need to check whether the mail item in question is actually a non delivery report. This is done by examining the Item Class of the mail. An NDR has an item class of REPORT.IPM.Note.NDR. Next, you need to retrieve the property PidTagParentKey from the item. This property contains the PidTagSearchKey of the original mail.

Unfortunately, the PidTagSearchKey cannot be used to bind to the item directly. Instead, you have to issue a FindItem operation to get it. Normally, sent items are all stored in the SentItems folder, so this narrows the search down.

Here is a sample method which describes the process:

 

   1:  class Program
   2:  {
   3:      private static readonly ExtendedPropertyDefinition PidTagParentKeyProperty = new ExtendedPropertyDefinition(0x25, MapiPropertyType.Binary);
   4:      private static readonly ExtendedPropertyDefinition PidTagSeachKeyProperty = new ExtendedPropertyDefinition(0x300B, MapiPropertyType.Binary);
   5:   
   6:   
   7:      private static bool TryGetOriginalMail(string ndrId, ExchangeService service, out Item originalItem)
   8:      {
   9:          byte[] value;
  10:   
  11:          var item = service.BindToItems(new[] {new ItemId(ndrId)},
  12:                                          new PropertySet(BasePropertySet.IdOnly,
  13:                                                          PidTagParentKeyProperty, ItemSchema.Subject)).First().Item;
  14:   
  15:          if (!item.TryGetProperty(PidTagParentKeyProperty, out value))
  16:          {
  17:              Console.Out.WriteLine("Correlationtoken not found.");
  18:              originalItem = null;
  19:              return false;
  20:          }
  21:   
  22:          originalItem = service
  23:              .FindItems(WellKnownFolderName.SentItems,
  24:                          new SearchFilter.IsEqualTo(PidTagSeachKeyProperty, Convert.ToBase64String(value)),
  25:                          new ItemView(1) {PropertySet = PropertySet.FirstClassProperties})
  26:              .FirstOrDefault();
  27:   
  28:          return true;
  29:      }
  30:  }

Posted by Henning Krause on Monday, June 27, 2011 8:12 PM, last modified on Monday, June 27, 2011 8:12 PM
Permalink | Post RSSRSS comment feed

Getting the body of an Email with a FindItems request

The FindItem operation (or the corresponding ExchangeService.FindItems method) does not return the body of an email by default. And when trying to explicitly request them via a custom propertyset, the call fails. Consider this method, which uses the EWS Managed API to execute a FindItems method to get every message from the inbox folder of a mailbox, fetching only the item id and the body:

   1: private static void GetAllItems(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:                            PropertySet = new PropertySet(BasePropertySet.IdOnly)
  12:                                              {
  13:                                                  ItemSchema.Body
  14:                                              }
  15:                        };
  16:  
  17:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  18:  
  19:         foreach (var item in result)
  20:         {
  21:             ProcessItem(item);
  22:         }
  23:         offset += pageSize;
  24:     } while (result.MoreAvailable);
  25: }

When executed, the ExchangeService instance throws a ServiceValidationException stating “The property Body cannot be used in FindItem requests”. The official workaround proposed by Microsoft is to use a FindItem request to get the item ids of the items in a folder and afterward issue a GetItem request containing all the item ids and request the body property. However, there is another solution: The body properties can be fetched by requesting the MAPI properties containing the body:

   1: private static ExtendedPropertyDefinition TextBodyProperty = new ExtendedPropertyDefinition(0x1000, MapiPropertyType.String);
   2: private static ExtendedPropertyDefinition HtmlBodyProperty = new ExtendedPropertyDefinition(0x1013, MapiPropertyType.Binary);

The first property definition can be used to fetch the text body of a mail. The second one fetches the Html body. The new GetAllItemsMethod now looks like this:

   1: private static void GetAllItems(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:                            PropertySet = new PropertySet(BasePropertySet.IdOnly)
  12:                                              {
  13:                                                  HtmlBodyProperty
  14:                                              }
  15:                        };
  16:  
  17:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  18:  
  19:         foreach (var item in result)
  20:         {
  21:             object body;
  22:             if (item.ExtendedProperties.TryGetValue(HtmlBodyProperty, out body))
  23:             {
  24:                 Console.Out.WriteLine("item.Body = {0}", Encoding.UTF8.GetString(Convert.FromBase64String((string)body)));
  25:             }
  26:         }
  27:         offset += pageSize;
  28:     } while (result.MoreAvailable);
  29: }

The HtmlBody is requested with the addtion of the HtmlBodyProperty to the ItemView in line 13. Since the Html body is stored in binary form and returned in Base64 encoded format, it needs to be decoded before it can be displayed. This is done in line 24. If the plaintext body is requested, the value of the body property in line 24 can simply be converted to a string.


Posted by Henning Krause on Tuesday, June 9, 2009 9:00 PM, last modified on Wednesday, April 20, 2011 6:19 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