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 PIN of a smartcard programmatically

There are certain scenarios where it’s not feasible to require user interaction when accessing the private key of a smartcard. For example, a service does have the ability to provide a user interface. To use a key from a smartcard where a PIN is required, it must be provided to the smartcard using an alternative way. For some operations in .NET, one can use the CspParameters class to provide the PIN. But not all APIs which require smartcard access provide a way to use that class. The SSLStream is such a case. Here is a small extension method which sets the PIN for an X509Certificate2 instance:

static class X509Certificate2Extension
{
    public static void SetPinForPrivateKey(this X509Certificate2 certificate, string pin)
    {
        if (certificate == null) throw new ArgumentNullException("certificate");
        var key = (RSACryptoServiceProvider)certificate.PrivateKey;

        var providerHandle = IntPtr.Zero;
        var pinBuffer = Encoding.ASCII.GetBytes(pin);

        // provider handle is implicitly released when the certificate handle is released.
        SafeNativeMethods.Execute(() => SafeNativeMethods.CryptAcquireContext(ref providerHandle, 
key.CspKeyContainerInfo.KeyContainerName,
key.CspKeyContainerInfo.ProviderName,
key.CspKeyContainerInfo.ProviderType,
SafeNativeMethods.CryptContextFlags.Silent)); SafeNativeMethods.Execute(() => SafeNativeMethods.CryptSetProvParam(providerHandle,
SafeNativeMethods.CryptParameter.KeyExchangePin,
pinBuffer, 0)); SafeNativeMethods.Execute(() => SafeNativeMethods.CertSetCertificateContextProperty(
certificate.Handle,
SafeNativeMethods.CertificateProperty.CryptoProviderHandle,
0, providerHandle)); } } internal static class SafeNativeMethods { internal enum CryptContextFlags { None = 0, Silent = 0x40 } internal enum CertificateProperty { None = 0, CryptoProviderHandle = 0x1 } internal enum CryptParameter { None = 0, KeyExchangePin = 0x20 } [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool CryptAcquireContext( ref IntPtr hProv, string containerName, string providerName, int providerType, CryptContextFlags flags ); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CryptSetProvParam( IntPtr hProv, CryptParameter dwParam, [In] byte[] pbData, uint dwFlags); [DllImport("CRYPT32.DLL", SetLastError = true)] internal static extern bool CertSetCertificateContextProperty( IntPtr pCertContext, CertificateProperty propertyId, uint dwFlags, IntPtr pvData ); public static void Execute(Func<bool> action) { if (!action()) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } }

The interesting piece here is, of course, the SetPinForPrivateKey method. It acquires a Win32 cryptographic context, sets the PIN and associates that pin with the certificate. Once you have an X509Certificate2 instance, you can set the PIN with just one line of code:

var certificate = GetCertificate();
certificate.SetPinForPrivateKey("123456");

The PIN is remembered until the X509Certificate2 instance is open.

Credits

This solution is taken from an stackoverflow article. I’ve just made an extension method out of it, converted all those magic numbers to enums and added a little bit of error handling.


Posted by Henning Krause on Monday, November 22, 2010 4:17 PM, last modified on Thursday, November 25, 2010 11:26 PM
Permalink | Post RSSRSS comment feed

Exchange Eventsink Foundation

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

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

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

image

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

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

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

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

Here is a sample event sink:

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

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

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

Be sure to customize the ApplicationName attribute, though.

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

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

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

Debugging

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

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

Licensing

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

Downloads


Tags: , ,

Technorati: , ,

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

Retrieving the size of a mailbox via WebDAV

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

The procedure to retrieve the cumulative folder size is this:

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

Here is a C# program which demonstrates this:

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

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