InfiniTec - Henning Krauses Blog

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

Searching the Global Address List - C# Edition

A long time ago, I wrote an article on How to get the Global Address List programatically. The theory behind that article is still valid, but it only features a VBScript example. Since someone from the Microsoft Exchange Forum had trouble converting it to C#, I fired up Visual Studio and hacked something together. The result is somewhat more complete than the VBScript example, because it allows access to all address lists, not just the default global address list (GAL).

Here we go:

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;

namespace AddressListSample
{
    public class ActiveDirectoryConnection
    {
        public DirectoryEntry GetLdapDirectoryEntry(string path)
        {
            return GetDirectoryEntry(path, "LDAP");
        }

        public DirectoryEntry GetGCDirectoryEntry(string path)
        {
            return GetDirectoryEntry(path, "GC");
        }

        private DirectoryEntry GetDirectoryEntry(string path, string protocol)
        {
            var ldapPath = string.IsNullOrEmpty(path) ? string.Format("{0}:", protocol) : string.Format("{0}://{1}", protocol, path);
            return new DirectoryEntry(ldapPath);
        }
    }

    public class ExchangeAddressListService
    {
        private readonly ActiveDirectoryConnection _Connection;

        public ExchangeAddressListService(ActiveDirectoryConnection connection)
        {
            if (connection == null) throw new ArgumentNullException("connection");
            _Connection = connection;
        }

        public IEnumerable<AddressList> GetGlobalAddressLists()
        {
            return GetAddressLists("CN=All Global Address Lists");
        }

        public IEnumerable<AddressList> GetAllAddressLists()
        {
            return GetAddressLists("CN=All Address Lists");
        } 
        public IEnumerable<AddressList> GetSystemAddressLists()
        {
            return GetAddressLists("CN=All System Address Lists");
        }

        private IEnumerable<AddressList> GetAddressLists(string containerName)
        {
            string exchangeRootPath;
            using (var root = _Connection.GetLdapDirectoryEntry("RootDSE"))
            {
                exchangeRootPath = string.Format("CN=Microsoft Exchange, CN=Services, {0}", root.Properties["configurationNamingContext"].Value);
            }
            string companyRoot;
            using (var exchangeRoot = _Connection.GetLdapDirectoryEntry(exchangeRootPath))
            using (var searcher = new DirectorySearcher(exchangeRoot, "(objectclass=msExchOrganizationContainer)"))
            {
                companyRoot = (string) searcher.FindOne().Properties["distinguishedName"][0];
            }

            var globalAddressListPath = string.Format(containerName + ",CN=Address Lists Container, {0}", companyRoot);
            var addressListContainer = _Connection.GetLdapDirectoryEntry(globalAddressListPath);

            using (var searcher = new DirectorySearcher(addressListContainer, "(objectClass=addressBookContainer)"))
            {
                searcher.SearchScope = SearchScope.OneLevel;
                using (var searchResultCollection = searcher.FindAll())
                {
                    foreach (SearchResult addressBook in searchResultCollection)
                    {
                        yield return
                            new AddressList((string) addressBook.Properties["distinguishedName"][0], _Connection);
                    }
                }
            }
        }
    }

    public class AddressList
    {
        private readonly ActiveDirectoryConnection _Connection;
        private readonly string _Path;

        private DirectoryEntry _DirectoryEntry;

        internal AddressList(string path, ActiveDirectoryConnection connection)
        {
            _Path = path;
            _Connection = connection;
        }

        private DirectoryEntry DirectoryEntry
        {
            get
            {
                if (_DirectoryEntry == null)
                {
                    _DirectoryEntry = _Connection.GetLdapDirectoryEntry(_Path);
                }
                return _DirectoryEntry;
            }
        }

        public string Name
        {
            get { return (string) DirectoryEntry.Properties["name"].Value; }
        }

        public IEnumerable<SearchResult> GetMembers(params string[] propertiesToLoad)
        {
            var rootDse = _Connection.GetGCDirectoryEntry(string.Empty);
            var searchRoot = rootDse.Children.Cast<DirectoryEntry>().First();
            using (var searcher = new DirectorySearcher(searchRoot, string.Format("(showInAddressBook={0})", _Path)))
            {
                if (propertiesToLoad != null)
                {
                    searcher.PropertiesToLoad.AddRange(propertiesToLoad);
                }
                searcher.SearchScope = SearchScope.Subtree;
                searcher.PageSize = 512;
                do
                {
                    using (var result = searcher.FindAll())
                    {
                        foreach (SearchResult searchResult in result)
                        {
                            yield return searchResult;
                        }
                        if (result.Count < 512) break;
                    }
                } while (true);
            }
        }
    }

    internal class Program
    {
        private static void Main()
        {
            var connection = new ActiveDirectoryConnection();
            var service = new ExchangeAddressListService(connection);
            foreach (var addressList in service.GetGlobalAddressLists())
            {
                Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
                foreach (var searchResult in addressList.GetMembers())
                {
                    Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
                }
            }

            foreach (var addressList in service.GetAllAddressLists())
            {
                Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
                foreach (var searchResult in addressList.GetMembers())
                {
                    Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
                }
            }

            foreach (var addressList in service.GetSystemAddressLists())
            {
                Console.Out.WriteLine("addressList.Name = {0}", addressList.Name);
                foreach (var searchResult in addressList.GetMembers())
                {
                    Console.Out.WriteLine("\t{0}", searchResult.Properties["name"][0]);
                }
            }
        }
    }
}

The sample wraps the whole logic up into two classes: ExchangeAddressListService and AddressList. The first serves as some kind of entry point and the latter allows access to the members of a list.

Hope this helps…


Posted by Henning Krause on Tuesday, October 25, 2011 8:22 PM, last modified on Tuesday, October 25, 2011 8:22 PM
Permalink | Post RSSRSS comment feed

Setting the Homepage of an Exchange folder using the EWS Managed API

A while ago, Glen posted an article on his blog on how to set homepage of a folder using ADO and later he posted a version of that script which uses the EWS managed API to do this in this MSDN forum thread. However, he wrote it for the first version of the API, and the EWS Managed API 1.1 has a slightly different object model. Since someone on the MSDN forums had difficulties to update the script to work with the EWS Managed API 1.1, I thought I just post an updated version here:

private static void SetFolderHomePage(IEnumerable<string> pathFragments, string url, ExchangeService service)
{
    var folderWebviewinfoProperty = new ExtendedPropertyDefinition(14047, MapiPropertyType.Binary);
    var root = Folder.Bind(service, WellKnownFolderName.MsgFolderRoot);
    var targetFolder = root;
    foreach (var fragment in pathFragments)
    {
        var result = service.FindFolders(targetFolder.Id, new SearchFilter.IsEqualTo(FolderSchema.DisplayName, fragment), new FolderView(1));
        if (result.TotalCount == 0)
        {
            throw new InvalidOperationException(string.Format("Folder fragment {0} was not found.", fragment));
        }
        targetFolder = result.Folders[0];
    }

    targetFolder.SetExtendedProperty(folderWebviewinfoProperty, EncodeUrl(url));
    targetFolder.Update();
}

private static byte[] EncodeUrl(string url)
{
    var writer = new StringWriter();
    var dataSize = ((ConvertToHex(url).Length / 2) + 2).ToString("X2");

    writer.Write("02"); // Version
    writer.Write("00000001"); // Type
    writer.Write("00000001"); // Flags
    writer.Write("00000000000000000000000000000000000000000000000000000000"); // unused
    writer.Write("000000");
    writer.Write(dataSize);
    writer.Write("000000");
    writer.Write(ConvertToHex(url));
    writer.Write("0000");
    
    var buffer = HexStringToByteArray(writer.ToString());
    return buffer;
}

private static string ConvertToHex(string input)
{
    return string.Join(string.Empty, input.Select(c => ((int) c).ToString("x2") + "00").ToArray());
}

private static byte[] HexStringToByteArray(string input)
{
    return Enumerable
        .Range(0, input.Length/2)
        .Select(index => byte.Parse(input.Substring(index*2, 2), NumberStyles.AllowHexSpecifier)).ToArray();
}

You can set the homepage of a folder by calling the SetFolderHomepage method:

SetFolderHomePage(service, new[] {"InfiniTec blog"}, http://www.infinitec.de);

Posted by Henning Krause on Wednesday, October 5, 2011 7:30 PM, last modified on Wednesday, October 5, 2011 7:34 PM
Permalink | Post RSSRSS comment feed

FindItems Helper method for the Exchange Managed API

I’ve seen quite a few samples for using the FindItems method of the EWS Managed API, either on StackOverflow or in the Exchange development forum. One of the most common error I see is that people disable paging by specifying creating an ItemView like this:

var view = new ItemView(int.MaxValue);

Don’t do that! A query using this ItemView will put a severe burden on the Exchange server if the folder being queried contains many items. The pageSize parameter is there for a reason.

Instead of querying all items at once, one should do a proper paging: Query, say, 512 items and process them. Then, query the next 512 items. Thanks to iterators in C#, the process of retrieving the items can be completely separated away from the processing.

If you are not familiar with C# iterators, have a look at this article. In essence, the yield operator allows for deferred execution, something you might heard from with regards to LINQ.

Here is a helper method which uses this technique to iterate over the contents of an Exchange folder, requesting 512 items per batch and returning them to the caller. When used properly, this method uses very little resources on the Exchange server as well as on the client. Additionally, it allows for querying the body of an item. In this case, instead of executing a FindItems request, it executes a FindItems request and a LoadPropertiesForItems request per batch.

Due to the nature of the method, you should not put the items returned from the methods in a List, as this would consume a lot of memory on the client. Instead process the items one by one.

public static class ExchangeServiceExtension
{
    public static IEnumerable<Item> FindItems(this ExchangeService service, WellKnownFolderName folderName, PropertySet propertySet, SearchFilter searchFilter, ItemTraversal traversal = ItemTraversal.Shallow, params KeyValuePair<PropertyDefinition, SortDirection>[] orderBy)
    {
        return FindItems(service, new FolderId(folderName), propertySet, searchFilter, traversal, orderBy);
    }

    public static IEnumerable<Item> FindItems(this ExchangeService service, FolderId folderId, PropertySet propertySet, SearchFilter searchFilter = null, ItemTraversal traversal = ItemTraversal.Shallow, params KeyValuePair<PropertyDefinition, SortDirection>[] orderBy)
    {
        if (service == null) throw new ArgumentNullException("service");
        if (folderId == null) throw new ArgumentNullException("folderId");
        if (propertySet == null) throw new ArgumentNullException("propertySet");

        PropertySet propertySetToQuery;

        // If the body or unique body is requested, the FindItems method cannot be used to query the 
        // propertyset. Instead a GetItem operation is required. The propertyset IdOnly is used in this case
        // for the FindItems operation.
        if (propertySet.Contains(ItemSchema.Body) || propertySet.Contains(ItemSchema.UniqueBody))
        {
            propertySetToQuery = PropertySet.IdOnly;
        }
        else
        {
            propertySetToQuery = propertySet;
        }

        var itemView = new ItemView(512) { PropertySet = propertySetToQuery, Traversal = traversal };
        
        if (orderBy != null)
        {
            foreach (var order in orderBy)
            {
                itemView.OrderBy.Add(order.Key, order.Value);
            }
        }

        bool hasMoreItems;
        do
        {
            var result = service.FindItems(folderId, searchFilter, itemView);
            if (propertySetToQuery == PropertySet.IdOnly)
            {
                // Load the properties, including the body using a GetItem request.
                service.LoadPropertiesForItems(result, propertySet);
            }
            foreach (var item in result)
            {
                yield return item;
            }
            hasMoreItems = result.MoreAvailable;
            itemView.Offset += 512;
        } while (hasMoreItems);
    }
}

As you can see, this method uses several .NET 4.0 features: Optional parameters and extension methods. So, if you are still using .NET 2.0, you’ll need to modify the code a little bit.

To use these methods, insert this code into your solution. In the class where you want to use the methods, add a using to the namespace containing the ExchangeServiceExtension. Both methods will then appear in the code completion window on instances of the ExchangeService class:

CodeCompletion

In the following example, the method is used to iterate over all items in the Inbox folder and printing the items to the console:

var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject));

foreach (var item in items)
{
    Console.Out.WriteLine("Subject = {0}", item.Subject);
}

Since the last parameters are optional, they can be omitted. This example adds a sort clause to the call:

var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject), 
    orderBy: new KeyValuePair<PropertyDefinition, SortDirection>(ItemSchema.Subject, SortDirection.Ascending));

Of course, a SearchFilter and an ItemTraversal can also be specified:

var items = service.FindItems(WellKnownFolderName.Inbox, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Subject), 
    new SearchFilter.ContainsSubstring(ItemSchema.Subject, "test"), ItemTraversal.Associated, 
    new KeyValuePair<PropertyDefinition, SortDirection>(ItemSchema.Subject, SortDirection.Ascending));

Posted by Henning Krause on Thursday, September 8, 2011 11:15 PM, last modified on Thursday, September 8, 2011 11:15 PM
Permalink | Post RSSRSS comment feed

Custom Mail headers and EWS

The mail headers are exposed on the EmailMessage as first class property: InternetMessageHeaders. However, Exchange also stores each mail header in its own property and defines a unique property set for it: PS_INTERNET_HEADERS.

Here is a property definition for a hypothetical custom property:

private static readonly ExtendedPropertyDefinition FrobProperty =
    new ExtendedPropertyDefinition(DefaultExtendedPropertySet.InternetHeaders, "X-Frob", MapiPropertyType.String);

If a mail is processed by Exchange with message header like this:

Subject: Test mail
X-Frob: Yes
From: "Henning Krause" <someone@infinitec.de>
To: "Someone <someone@example.local>

Exchange will parse the mail and set the value of the extended property defined above to “Yes”.

Retrieving the custom property is as simple as this:

var item = Item.Bind(service, itemId,
    new PropertySet(BasePropertySet.FirstClassProperties, FrobProperty));

The value of the property can then be accessed using this line:

string frobValue;
if (item.TryGetProperty(FrobProperty, out frobValue))
{
    Console.Out.WriteLine("The item as a frob value of: {0}", frobValue);
}

You can even search for messages which have the header “X-Frob” set to “Yes”:

var itemId = folder.FindItems(new SearchFilter.IsEqualTo(FrobProperty, "yes"), 
    new ItemView(10) {PropertySet = PropertySet.IdOnly});

This query will return the first ten messages which have said header set to “Yes”.

Note that Exchange has a limit for named properties which are the result of custom mail headers. You may get one of these entries in your EventLog at some point:

Event ID: 9666
Type: Warning
Category: General
Source: msgidNamedPropsQuotaWarning
Description: The number of named properties created for database "<database name>" is close to quota limit. Current number of named properties: <number of named properties>. Quota limit for named properties: <configured quota>. User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.

 

Event ID: 9667
Type: Error
Category: General
Source: msgidNamedPropsQuotaError
Description: Failed to create a new named property for database "<database name>" because the number of named properties reached the quota limit (<configured quota>). User attempting to create the named property: <user name>. Named property GUID: <GUID of named property>. Named property name/id: <name of named property>.

 

See this blog post on how to deal with this issue: http://blogs.technet.com/b/exchange/archive/2010/07/29/3410545.aspx.


Posted by Henning Krause on Monday, August 1, 2011 12:00 PM, last modified on Wednesday, July 27, 2011 5:57 PM
Permalink | Post RSSRSS comment feed

Replying with attachments to an email message with EWS

The EWS Managed API makes some tasks really easy – like replying to an existing email. Given the unique id of an item, this can be done this way:

var message = (EmailMessage) Item.Bind(service, new ItemId(uniqueId), PropertySet.FirstClassProperties);
var reply = message.CreateReply(false);
reply.BodyPrefix = "Response text goes here";
reply.SendAndSaveCopy();

But what if you want to send an attachment along with the reply? This is tricky, because the the instance created by the EmailMessage.CreateReply() method is of type ResponseMessage. Since its not an EmailMessage instance, it does not have an Attachment property. To add an attachment to this reply, it needs to be saved first. The Save method returns an EmailMessage instance. Attachments can be added to this message. Finally, the message can be sent to its destination.

var message = (EmailMessage) Item.Bind(service, new ItemId(uniqueId), PropertySet.FirstClassProperties);
var reply = message.CreateReply(false);
reply.BodyPrefix = "Response text goes here";
var replyMessage = reply.Save(WellKnownFolderName.Drafts);
replyMessage.Attachments.AddFileAttachment("d:\\inbox\\test.pdf");
replyMessage.Update(ConflictResolutionMode.AlwaysOverwrite);
replyMessage.SendAndSaveCopy();

Posted by Henning Krause on Monday, July 18, 2011 8:04 PM, last modified on Monday, July 18, 2011 8:04 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

Exchange WebServices Bug with Lineendings

When you fetch the body of a mail via Exchange WebServices you might have noticed the fact that the body of the mail has its line endings, which are normally represented by a CRLF (“\r\n” in C#) replaced with a simple LF (“\n”). For the text and HTML body, this is the expected behavior. But Exchange also enforces this behavior on custom properties, which was not intended.

Below is a small program that demonstrates the problem. It creates a new item in the drafts folder of the current users mailbox and then reads the content back. You’ll notice the missing \r characters when you examine the propertyValue property.

   1: using System;
   2: using System.Linq;
   3: using System.Net;
   4: using Microsoft.Exchange.WebServices.Data;
   5:  
   6: namespace TestApplication
   7: {
   8:     internal class Program
   9:     {
  10:         private static void Main()
  11:         {
  12:             ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
  13:             var service = new ExchangeService(ExchangeVersion.Exchange2007_SP1)
  14:                           {
  15:                           Url = new Uri("https://w2k3x64/ews/exchange.asmx"),
  16:                           UseDefaultCredentials = true,
  17:                           };
  18:             Folder folder = Folder.Bind(service, WellKnownFolderName.Drafts);
  19:             var item = new PostItem(service) {Subject = "test", Body = "Line1\r\nLine2"};
  20:  
  21:             var definition = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.PublicStrings, "TestProperty",
  22:                                                             MapiPropertyType.String);
  23:             item.ExtendedProperties.Add(definition, "Line1\r\nLine2");
  24:             item.Save(folder.Id);
  25:  
  26:             ItemId id = item.Id;
  27:             item = PostItem.Bind(service, id, new PropertySet(BasePropertySet.FirstClassProperties, definition));
  28:             string propertyValue = (from property in item.ExtendedProperties 
  29:                                     where property.PropertyDefinition == definition 
  30:                                     select property.Value).First();
  31:  
  32:             Console.Out.WriteLine("item.Body = {0}", item.Body);
  33:             Console.Out.WriteLine("propertyValue = {0}", propertyValue);
  34:  
  35:             Console.ReadLine();
  36:         }
  37:     }
  38: }

So, if you are struggling with this ‘anomaly’, you need to open a case with the Microsoft products support (PSS) to get a fix.


Posted by Henning Krause on Tuesday, August 25, 2009 9:04 PM, last modified on Tuesday, August 25, 2009 9:04 PM
Permalink | Post RSSRSS comment feed

Troubleshooting Push notifications failures

It can sometimes be tricky to get Exchange push notifications working. The development of a listener is only part of the solution. The other part is getting the notifications from the Exchange server to the listener, especially when the listener expects the notifications on a secure channel. Exchange is of no particular help here. The only error message that is generated when Exchange can’t send a notification message to a listener is something like this:

image

This message is not helpful at all. Ok – Exchange could not send a notification a specific subscription. But why?

That’s why I wrote a small utility that should be able to diagnose the three most common problems:

  • The Exchange server cannot reach the notification listener because a firewall is blocking the access.
  • The host specified in the callback address can not be resolved to an IP address
  • If the callback address of the listener is secured using TLS/SSL, Exchange will not send notifications if the server certificate used by the listener is not trusted.

Instructions:

Copy the program on the Exchange server. Open a command prompt and start the program. It expects the address of the listener as the first parameter. The execution of the program can take up to one minute.

Download the program here:

   (105kb)

The source code can be downloaded from CodePlex.


Posted by Henning Krause on Sunday, June 21, 2009 12:14 AM, last modified on Sunday, June 21, 2009 10:59 AM
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

FindItems and SyncFolderItems performance

By default, Exchange returns a rather large set of properties with each item during a FindItem or SyncFolderItems request. If the query returns a large set of items, this slows down the entire process: On one hand, Exchange needs to get all properties from the store and secondly, all the data needs to be sent over the wire to the calling application. The calling application can specify which items to fetch for a FindItem or SyncFolderItems request by specifying it in the PropertySet parameter. The default property set looks like this:

   1: var PropertySet = new PropertySet(BasePropertySet.FirstClassProperties)

To request only the property id of the items, use this declaration instead:

   1: var PropertySet = new PropertySet(BasePropertySet.IdOnly)

I’ve measured the FindItems call with the Exchange server at my workplace from home. So the call went over the Internet using a 2 MBit connection (on both sides). The inbox folder of my mailbox currently contains approximately 4500 items and I have repeated the measurement two times for each of the propertysets. When querying only for the id of the items, the entire process took 46 seconds to complete. On the other hand, when querying the default set of properties, the whole process took between 1:37 minutes and 1:41 minutes to complete.

But item id is seldom the only property needed. Luckily, the additional properties can be specified on the property set:

   1: var PropertySet = new PropertySet(BasePropertySet.IdOnly)
   2:                   {
   3:                      ItemSchema.Subject,
   4:                      ItemSchema.DateTimeReceived
   5:                   }

Executing this query on my mailbox, took about 50 seconds to complete. This is a significant improvement of the default property set.


Posted by Henning Krause on Sunday, June 7, 2009 12:56 PM, last modified on Saturday, November 27, 2010 6:35 AM
Permalink | Post RSSRSS comment feed