Tuesday, March 27, 2007

Managing Exchange 2007 Recipients with C#

During our Exchange 2007 implementation I was faced with the task of integrating Exchange 2007 recipient management with our in-house identity management systems. Our active directory provisioning code was already written in .Net and implemented CDOEXM for recipient management. One of the things that concerned me the most about migrating to Exchange 2007 was the ability to manage recipients from within the existing code. CDOEXM is not supported for managing Exchange 2007 recipients and the idea of invoking the Exchange management shell from .Net code was a foreign concept to me. Information on how to do so was sparse. Many many thanks to Vivek Sharma for posting sample code before anything else was available. My early code was based directly off his examples. Since then I have created a few applications using these methods and I have learned quite a few lessons while doing so. Below are some of my experiences and customizations that made the process much simpler.

Web References


First, here are some helpful links to get started with invoking the management shell from C# code:

Creating Applications that Use the Default Host
http://msdn2.microsoft.com/en-us/library/ms714671.aspx

Mstehle's Introduction to Exchange Powershell Automation
Part 1: http://blogs.msdn.com/mstehle/archive/2007/01/25/fyi-introduction-to-exchange-powershell-automation-part-1.aspx
Part 2: http://blogs.msdn.com/mstehle/archive/2007/01/25/outbox-introduction-to-exchange-powershell-automation-part-2.aspx

Sample Code: Calling Exchange cmdlets from .Net code
http://www.viveksharma.com/techlog/2006/07/27/sample-code-calling-exchange-cmdlets-from-net-code

Caveats


  • Creating the necessary Runspace and RunspaceInvoke instances will take 2-3 seconds to get setup in a compiled application.
  • It can take 6-9 seconds to execute the first Exchange command on a computer with only the Exchange 2007 management tools installed. You may also receive the following errors in the event log:

    Event Type: Error
    Event Source: MSExchange ADAccess
    Event Category: General
    Event ID: 2152
    Date: 3/26/2007
    Time: 7:38:52 PM
    User: N/A
    Computer: HASTY
    Description:
    Process eIDAD.exe (PID=2984). A remote procedure call (RPC) request to the Microsoft Exchange Active Directory Topology service failed with error 1753 (Error 6d9 from HrGetServersForRole). Make sure that the Remote Procedure Call (RPC) service is running. In addition, make sure that the network ports that are used by RPC are not blocked by a firewall.


    Event Type: Error
    Event Source: MSExchange Common
    Event Category: Devices
    Event ID: 106
    Date: 3/26/2007
    Time: 7:38:57 PM
    User: N/A
    Computer: HASTY
    Description:
    Performance counter updating error. Counter name is Latest Exchange Topology Discovery Time in Seconds, category name is MSExchange Topology. Optional code: 2.

  • Invoking the Runspace and adding the Exchange management snap-in does not give you the same environment as opening the Exchange Management Shell. One of the most striking differences is that the $AdminSessionADSettings environment settings are not available. By default, all Exchange management cmdlets will search the entire forest. You can best replicate this environment by starting a normal PowerShell instance and add the Microsoft.Exchange.Management.Powershell.Admin snap-in. (Run “Add-PSSnapin -Name:’Microsoft.Exchange.Management.PowerShell.Admin’ ”).
  • To compile your code you must add a reference for Systems.Management.Automation. By default this is added to the global assembly cache when PowerShell is installed. The best way to add this DLL as a reference is to manually edit the .csproj file associated with the project and add the following line to the references itemgroup.
    <reference include="System.Management.Automation">

    Adding the reference in this manner will ensure that you are always using the appropriate DLL whether you are running on a 32 or 64 bit machine.


Birth of the wrapper class


My first attempt at integrating recipient management into our identity management provisioning code worked well but I paid quite a time penalty when invoking the shell and issuing commands to update/set attributes on each account. Updating a single account was not too bad (~1 second per account) after the Runspace was completely setup and all the Exchange DLLs were loaded. However, when updating all 60,000+ accounts the time penalty added up and became an unacceptable bottleneck. To prevent this, I implemented logic to check if each attribute needed to be updated before invoking the command to set the attribute. This logic removed the bottleneck and very soon the code was running just as quickly as before; except for the time needed to create the Runspace. These optimizations made the Runspace unnecessary for most instances when an update was run on a single account and no Exchange attributes were changed. Thus the wrapper class was setup to store a Runspace that would only be initialized if a command was invoked.

Notes About the Wrapper Class


  • The wrapper class is a singleton and must me instantiated as such. Because the class is a singleton, only one Runspace will be created no matter how many times the class is instantiated.
  • The Runspace is only created once the first command is invoked. The same is true with RunspaceInvoke.
  • The wrapper class combines methods from both the Runspace and RunspaceInvoke classes.


The Wrapper Class


using System;

using System.IO;

using System.Text;

using System.Reflection;

using System.Diagnostics;

using System.Collections;

using System.Collections.Generic;

using System.Collections.ObjectModel;

using System.Management.Automation;

using System.Management.Automation.Host;

using System.Management.Automation.Runspaces;

 

namespace EMSDemo.Utility

{

   public sealed class ExchangeManagementShellWrapper

   {

      #region Variable Declaration

      private RunspaceConfiguration rc;         //RunspaceConfiguration

      private Runspace r;                       //Runspace

      private RunspaceInvoke ri;                //RunspaceInvoke

      private static readonly ExchangeManagementShellWrapper instance = new ExchangeManagementShellWrapper(); //Singleton instance

      #endregion

 

      private ExchangeManagementShellWrapper(){}

 

      public static ExchangeManagementShellWrapper Instance

      {

         get

         {

            return instance;

         }

      }

 

      private void InitializeRunspace()

      {

         //Setup Runspace environment for Exchange Management Shell

         AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyResolver);

        

         //Create RunspaceConfiguration

         rc = RunspaceConfiguration.Create();

 

         //Add PSSnapIn for Exchange 2007 and check for warnings

         PSSnapInException warning;

         PSSnapInInfo info = rc.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);

         if (warning != null)

         {

            // A warning is not expected, but if one is detected

            // write the warning and return.

            System.Console.Write(warning.Message);

            return;

         }

 

         //Create and open runspace

         r = RunspaceFactory.CreateRunspace(rc);

         r.Open();

 

      }

 

      private void InitializeRunspaceInvoke()

      {

         //Create runspace if it has not already been created

         if (r == null) { InitializeRunspace(); }

 

         //Create RunspaceInvoke

         ri = new RunspaceInvoke(r);

      }

 

      public ICollection<PSObject> RunspaceInvoke(string EMSCommand)

      {

         //Create RunspaceInvoke if it has not already been created

         if (ri == null) { InitializeRunspaceInvoke(); }

 

         //Invoke the commmand and return the results

         return ri.Invoke(EMSCommand);

      }

 

      public ICollection<PSObject> RunspaceInvoke(string EMSCommand, IEnumerable input)

      {

         //Create RunspaceInvoke if it has not already been created

         if (ri == null) { InitializeRunspaceInvoke(); }

 

         //Invoke the commmand and return the results

         return ri.Invoke(EMSCommand, input);

      }

 

      public ICollection<PSObject> RunspaceInvoke(string EMSCommand, out IList errors)

      {

         //Invoke the RunspaceInvoke method with a null input

         return RunspaceInvoke(EMSCommand, null, out errors);

      }

     

      public ICollection<PSObject> RunspaceInvoke(string EMSCommand, IEnumerable input, out IList errors)

      {

         //Create RunspaceInvoke if it has not already been created

         if (ri == null) { InitializeRunspaceInvoke(); }

 

         //Invoke the commmand and return the results and errors

         return ri.Invoke(EMSCommand, input, out errors);

      }

 

      public ICollection<PSObject> PipelineInvoke(Collection<Command> EMSCommands, out PipelineReader<object> Errors)

      {

         //Create Runspace if it has not already been created

         if (r == null) { InitializeRunspace(); }

 

         //Create Pipeline and add the commands

         Pipeline pipeline = r.CreatePipeline();

         foreach (Command item in EMSCommands)

         {

            pipeline.Commands.Add(item);

         }

 

         //Invoke the commands and return the results and errors

         ICollection<PSObject> results = pipeline.Invoke();

         Errors = pipeline.Error;

         pipeline = null;

         return results;

      }

 

      public ICollection<PSObject> PipelineInvoke(Collection<Command> EMSCommands)

      {

         //Create Runspace if it has not already been created

         if (r == null) { InitializeRunspace(); }

 

         //Create Pipeline and add the commands

         Pipeline pipeline = r.CreatePipeline();

         foreach (Command item in EMSCommands)

         {

            pipeline.Commands.Add(item);

         }

        

         //Invoke the commands and return the results

         return pipeline.Invoke();

      }

 

      public ICollection<PSObject> PipelineInvoke(Command EMSCommand, out PipelineReader<object> Errors)

      {

         //Create Runspace if it has not already been created

         if (r == null) { InitializeRunspace(); }

 

         //Create Pipeline and add the command

         Pipeline pipeline = r.CreatePipeline();

         pipeline.Commands.Add(EMSCommand);

 

         //Invoke the command and return the results and errors

         ICollection<PSObject> results = pipeline.Invoke();

         Errors = pipeline.Error;

         pipeline = null;

         return results;

      }

 

      public ICollection<PSObject> PipelineInvoke(Command EMSCommand)

      {

         //Create Runspace if it has not already been created

         if (r == null) { InitializeRunspace(); }

 

         //Create Pipeline and add teh command

         Pipeline pipeline = r.CreatePipeline();

         pipeline.Commands.Add(EMSCommand);

 

         //Invoke the command and return the results

         return pipeline.Invoke();

      }

 

      public void Dispose()

      {

         //Close the Runspace and cleanup

         r.Close();

         ri = null;

         r = null;

      }

 

      private static System.Reflection.Assembly AssemblyResolver(object p, ResolveEventArgs args)

      {

         //Add path for the Exchange 2007 DLLs

         if (args.Name.Contains("Microsoft.Exchange"))

         {

            return Assembly.LoadFrom(Path.Combine("C:\\Program Files\\Microsoft\\Exchange Server\\bin\\", args.Name.Split(',')[0] + ".dll"));

         }

         else

         {

            return null;

         }

      }

   }

}



Examples


Each example will perform the same task and produce the same results using both the RuspaceInvoke and PipelineInvoke methods.

Initializing the Exchange Management Shell Wrapper

Singletons do not have a constructor so you must create an ‘instance’.

static void Main(string[] args)

{

  //Initialize the ExchangeManagementShellWrapper

  ExchangeManagementShellWrapper ems = ExchangeManagementShellWrapper.Instance;

  ICollection<PSObject> results;

  ...



Invoking a Single Command

In this example both the RunspaceInvoke and PipelineInvoke methods are used to get information about the mailbox for ‘knsmith’. In each foreach loop are different methods for accessing the data within the PSObject.

//Use the RunspaceInvoke command with a command string

results = ems.RunspaceInvoke("Get-Mailbox knsmith");

foreach (PSObject item in results)

{

  Console.WriteLine(item.Members["Name"].Value.ToString());

}

 

//Use the PipelineInvoke command with a Command object

Command EMSCommand = new Command("Get-Mailbox");

EMSCommand.Parameters.Add("Identity", "knsmith");

results = ems.PipelineInvoke(EMSCommand);

foreach (PSObject item in results)

{

  //A different way of accessing property info from the results

  PSPropertyInfo prop = (PSPropertyInfo)item.Properties["Name"];

  if (prop != null)

  {

    Console.WriteLine(prop.Value.ToString());

  }

}



Invoking Commands With Errors Ouput

In this example each command will attempt to create a new file at c:\temp called test.ps1. A new file will be created it one does not already exist. If the file does exist, an error will be returned and the output will be: “Error: The file ‘C:\temp\test.ps1’ already exists.

//Using RunspaceInvoke with errors

IList IErrors;

results = ems.RunspaceInvoke("New-Item -Path:'c:\\temp\\test.ps1' -Type:File", out IErrors);

foreach (PSObject item in results)

{

  Console.WriteLine(item.Members["Name"].Value.ToString());

}

foreach (object item in IErrors)

{

  Console.WriteLine("Error: {0}", item.ToString());

}

 

//Using PipelineInvoke with errors

EMSCommand = new Command("New-Item");

EMSCommand.Parameters.Add("Path", "c:\\temp\\test.ps1");

EMSCommand.Parameters.Add("Type", "File");

PipelineReader<object> errors;

results = ems.PipelineInvoke(EMSCommand, out errors);

foreach (PSObject item in results)

{

  Console.WriteLine(item.Members["Name"].Value.ToString());

}

if (errors.Count > 0)

{

  foreach (object obj in errors.ReadToEnd())

  {

    Console.WriteLine("Error: {0}", obj.ToString());

  }

}



Invoking Multiple Commands

Invoking multiple commands using a pipe is one of my most common tasks in PowerShell. The RunspaceInvoke method is pretty straight forward as you can just pass in a command string the same as you use in the Exchange Management Shell. The PipelineInvoke method is a little different. You must create a Command object for each command you wish to issue. After the Command objects have been created, add them in the order they are to be executed to the collection.

//Using the RunspaceInvoke command with a command string

ems.RunspaceInvoke("Get-Mailbox knsmith | Set-Mailbox -EmailAddressPolicyEnabled:$True");

 

//Using PipelineInvoke with a Command Collection

Collection<Command> CommandsList = new Collection<Command>();

Command GetMailbox = new Command("Get-Mailbox");

GetMailbox.Parameters.Add("Identity", "knsmith");

CommandsList.Add(GetMailbox);

 

Command SetMailbox = new Command("Set-Mailbox");

SetMailbox.Parameters.Add("EmailAddressPolicyEnabled", true);

CommandsList.Add(SetMailbox);

 

ems.PipelineInvoke(CommandsList, out errors);



Passing an Input Object to the Runspace

The RunspaceInvoke method offers the added ability to pass in an input collection. The key to this is that you must start your command string with “$input | ” so that your input will be processed. This example will show how you can get the attributes for a mailbox, make a change and commit the changes by passing back the results and using the Set-Mailbox cmdlet.

//Use the RunspaceInvoke command with a command string

results = ems.RunspaceInvoke("Get-Mailbox knsmith");

foreach (PSObject item in results)

{

  //Set the property to false

  item.Members["UseDatabaseQuotaDefaults"].Value = false;

}

 

IList SetErrors;

 

//Save the changes we made to the results

//The $input variable will be equal to the 'results' variable we pass in

ems.RunspaceInvoke("$input | Set-Mailbox", results, out SetErrors);

 

foreach (object error in SetErrors)

{

  Console.WriteLine(error.ToString());

}



Download the wrapper class and example code. (Right-click and 'Save Target As')

Conclusion


Whether or not you decide to use this wrapper class I hope it at least helps you with examples of how to utilize the RunspaceInvoke and Pipeline methods for invoking the Exchange management shell.

--Nick

Thursday, March 22, 2007

Granting Blackberry Permissions on Exchange 2007

One of the requirements for deploying a Blackberry Enterprise Server is to grant the Blackberry service account privileges on user mailboxes. The Blackberry service account must be granted ‘full mailbox access’ and the ‘send-as’ permissions to work properly. Here are two methods of accomplishing this task using PowerShell.

Single User Method



[PS] C:\ >Add-MailboxPermission username@domain –user domain\besadmin –AccessRights FullAccess
[PS] C:\ >Add-ADPermission username@domain –user domain\besadmin -ExtendedRights Send-As, Receive-As


The first command will grant the Blackberry service account (besadmin) full mailbox access to the user’s mailbox. The second command will assign the appropriate active directory permissions so the service account can send and receive email as the user.

Server Method



[PS] C:\ >Get-MailboxServer | Add-ADPermission -User domain\besadmin -AccessRights GenericRead, GenericWrite -ExtendedRights Send-As, Receive-As, ms-Exch-Store-Admin


This method will grant all the necessary permissions to the service account on ALL mailboxes hosted on Exchange 2007 servers.

--Nick