Posts tagged: inline code

Patch and reboot script for an entire Exchange environment, Part 4

By , January 20, 2012 8:58 AM

This post is part of a multi-part series:

Part 1
Part 2
Part 3
Part 4 (this post)

After enabling transcript logging, loading the Exchange snap-in, and connecting to SCOM, the server processing begins.  This is grouped by server role and production vs. DR.

ProcessServerGroup $HTCAS 'HTCAS'
ProcessServerGroup $UM 'UM'
ProcessServerGroup $MB 'MB'
BalanceDB
ProcessServerGroup $DRHTCAS 'HTCAS'
ProcessServerGroup $DRUM 'UM'
ProcessServerGroup $DRMB 'MB'

Based on my environment’s configuration where hub transport and client access server are colocated, the production HT/CAS servers are processed, then the UM servers, and then mailbox servers.  When the production mailbox servers are complete, the databases are re-balanced according to activation preference.  Then the DR servers are processed.

Let’s go through the ProcessServerGroup function.

function ProcessServerGroup($serverGroup,$groupType)
	{
	foreach ($server in $serverGroup)
		{
		Write-Host "*******************Beginning processing of $server.*******************"
		$Host.UI.RawUI.WindowTitle = "Processing $server"
		if ($bSendStatusUpdates)
			{
			SendStatus "Processing beginning" $server
			}

The function is called by passing the array that contains the server names, along with the role they serve. Looping through each server in the array, the display reflects the status. If status updates are enabled, line 9 calls a function with the status and the server name; here is the SendStatus function:

function SendStatus($details,$server)
	{
	$mailSubject = "$server status update"
	$mailBody = "$server: $details"
	$smtp = New-Object Net.Mail.SmtpClient($mailServer)
	$smtp.Send($mailSender,$statusRecipients,$mailSubject,$mailBody)
	}

The only noteworthy thing about the previous function is the use of the SmtpClient .NET class instead of the Send-MailMessage cmdlet. This is because of the different encoding used between them. When sending to a mobile device via SMS, the cmdlet’s encoding causes non-printable characters to be displayed in the SMS body. Using the .NET class avoids this.

The next section of the ProcessServerGroup function is for SCOM. If SCOM updates are enabled and the remote PowerShell session to the RMS is established, a function to start maintenance mode is called:

		if ($bUpdateSCOM -and $global:bSCOMSessionOpen)
			{
			StartSCOMMaintenance $server
			}

And here is that function:

function StartSCOMMaintenance($nodeName)
	{
	WriteTime;Write-Host 'Putting server into maintenance mode in SCOM...'
	$ErrorActionPreference = 'Stop'
	try
		{
		Invoke-Command -ScriptBlock {param($nodeName) $serverCrit = "DisplayName like '$nodeName%'"} -ArgumentList $nodeName -Session $global:scomSession
		Invoke-Command -ScriptBlock {$monObj = Get-MonitoringObject -MonitoringClass $monClass -Criteria $serverCrit} -Session $global:scomSession
		Invoke-Command -ScriptBlock {param($duration) $now = Get-Date;$maintStartTime = $now.ToUniversalTime();$maintEndTime = $now.AddMinutes($duration).ToUniversalTime()} -ArgumentList $maintModeDuration -Session $global:scomSession
		Invoke-Command -ScriptBlock {New-MaintenanceWindow -MonitoringObject $monObj -StartTime $maintStartTime -EndTime $maintEndTime -Reason PlannedApplicationMaintenance -Comment 'Patch and reboot'}  -Session $global:scomSession
		WriteTime;Write-Host 'Server successfully put into maintenance mode in SCOM.'
		$script:bMaintModeActive = $true
		}
	catch
		{
		WriteTime;Write-Host 'Warning: Server could not be put into maintenance mode in SCOM.' -ForegroundColor Yellow
		$script:bMaintModeActive = $false
		}
	$ErrorActionPreference = 'Continue'
	}

Lines 7-10 send commands to the remote session. It took some time to figure out how to do this because it isn’t well documented. The gist is that the SCOM object for a server has to be found, then a new maintenance window created. You can’t have an open-ended window, so the duration is set to the variable defined earlier. If the server is successfully put into maintenance mode, Line 12 sets a variable with this state. If any of the previous steps fail, a warning is output to the screen. I don’t treat this as a fatal error, so the patching will continue.

The next post in the series will continue with the ProcessServerGroup function and moving active database copies if a server with the mailbox role is being processed.

Download the complete script:

  Reboot-ExchangeServers.zip (4.8 KiB)

Patch and reboot script for an entire Exchange environment, Part 3

By , January 12, 2012 11:08 AM

This post is part of a multi-part series:

Part 1
Part 2
Part 3 (this post)
Part 4

The first part of the script body gets the current window title, then checks if transcript is running, starting it if not.

#Region Script Body
$savedTitle = $Host.UI.RawUI.WindowTitle
if (!($global:transcriptIsRunning))
	{
	Start-Transcript -Path $transcriptLog -Append
	$global:transcriptIsRunning = $true
	}

Since the script can’t use remote PowerShell to connect to an Exchange server that will be rebooted, the snap-in is loaded locally.

#Load Exchange 2010 snap-in
if (-not (Get-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue))
	{Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010}

If the SCOM variable is set to true, then the remote PowerShell connection to the RMS is loaded.

if ($bUpdateSCOM)
	{
	WriteTime;Write-Host 'Option: SCOM maintenance mode support enabled.'
	LoadSCOMShell
	}

Throughout the script you will see that whenever something is output to the console it is prefixed with the time. This is accomplished with a function to format it the way I want to see it.

function WriteTime
	{
	$a = Get-Date -Format g
	Write-Host "$a`: " -NoNewline
	}

This is the function to connect to the SCOM RMS:

function LoadSCOMShell
	{
	WriteTime;Write-Host 'Creating remote session to SCOM server...'
	$ErrorActionPreference = 'Stop'
	try
		{
		$global:scomSession = New-PSSession -ComputerName $rmsName
		Invoke-Command -ScriptBlock {Add-PSSnapin 'Microsoft.EnterpriseManagement.OperationsManager.Client'} -Session $global:scomSession
		Invoke-Command -ScriptBlock {Set-Location 'OperationsManagerMonitoring::'} -Session $global:scomSession
		Invoke-Command -ScriptBlock {param($rmsName) New-ManagementGroupConnection -connectionString:$rmsName | Out-Null} -Session $global:scomSession -ArgumentList $rmsName
		Invoke-Command -ScriptBlock {param($rmsName) Set-Location $rmsName} -Session $global:scomSession -ArgumentList $rmsName
		Invoke-Command -ScriptBlock {$monClass = Get-MonitoringClass | Where-Object {$_.DisplayName -eq 'Windows Server'}} -Session $global:scomSession
		WriteTime;Write-Host "Successfully created remote session to SCOM server." -ForegroundColor Green
		$global:bSCOMSessionOpen = $true
		}
	catch
		{
		WriteTime;Write-Host 'Error: Unable to create remote session to SCOM server...' -ForegroundColor Red
		$global:bSCOMSessionOpen = $false
		}
	$ErrorActionPreference = 'Continue'
	}

The function creates a new session to the RMS, scoping the variable as $global so that if the script aborts and you restart it in the same shell, the session can still be used instead of creating a new one. Once the session is established, several script blocks are executed in it to load the SCOM snap-in and configure it so that a server can be put into maintenance mode. It took some time to figure out how to do all this because the PowerShell documentation for SCOM is very poor and code samples on other sites were for working with clusters or doing other tasks. If no error occurs during any of this, a variable is set to indicate that the session is working. If an error does occur, the script does not abort. I only treat this as a warning since an inability to put a server into maintenance shouldn’t keep the server from being patched. The status variable is set so that the script won’t try and put servers into maintenance mode if doing so will only result in an error anyway.

The next post in the series will get into the actual processing of the servers.
Download the complete script:

  Reboot-ExchangeServers.zip (4.8 KiB)

Patch and reboot script for an entire Exchange environment, Part 2

By , December 30, 2011 9:59 AM

This post is part of a multi-part series:

Part 1
Part 2 (this post)
Part 3

The first section, or region as used in PowerGUI, is where all the variables are defined.  The script will process servers based on their Exchange roles and whether they are in the production or standby (DR) data center.  For my environment, this means hub transport and client access are on the same server, while the mailbox and unified messaging roles are dedicated.

#Server groupings
[array]$HTCAS = 'prodhc01','prodhc02' #Production HT/CAS
[array]$UM = 'produ01','produ02' #Production UM
[array]$MB = 'prodm03','prodm04','prodm02','prodm01' #Production MB
[array]$DRHTCAS = 'drhc01','drhc02' #DR HT/CAS
[array]$DRUM = 'dru01' #DR UM
[array]$DRMB = 'drm01','drexm02' #DR MB

Each server grouping is an array that the code body will loop through.  To accommodate a grouping that might only have one server, the variables are cast as arrays.

The next part of the variables section is for putting the servers into maintenance mode in SCOM.

#SCOM settings
$bUpdateSCOM = $true #Set to false if SCOM is not used or servers should not be put into maintenance mode
$rmsName = 'rmsserver' #SCOM RMS to connect to if SCOM update is true
$maintModeDuration = 90 #Maximum duration, in minutes, of SCOM maintenance window

All of the variable declarations have trailing comments so you know what they are for, but they are also explained here.  Line 2 is for indicating whether you have SCOM in your environment or, even if you do, that you don’t want to execute that portion in the code body.  Line 3 is the name of the root management server to connect to with remote PowerShell.  Line 4 is for how long the server should be put into maintenance.  (When a server has finished rebooting and all is well, the server will be taken out of maintenance mode, so this is in case it can’t be done for some reason.)

Next is where non-mail variables are defined.

$WUCompDelay = 90 #Maximum minutes allowed for Windows Update to complete
$rebootMaxWait = 20 #Maximum minutes allowed for server to complete reboot
$serviceStartDelay = 3 #Minutes to wait for Exchange services to start after reboot
$serviceStartAttempts = 3 #Number of times to check for services started
$transcriptLog = 'c:\scripts\RebootScriptLog.txt' #Transcript file to record screen output and send in email

Line 1 specifies how long the script should wait for Windows Update to complete before aborting.  Earlier versions had this set to 20 minutes, then 30, then 45, increasing mostly to accommodate roll-up installation since compiling .NET images (aka the Ngen process) can take some time to complete.  You can set this to whatever you feel comfortable with, knowing that if you set it too short the script will abort when there may be nothing wrong with the server.  Line 2 is how long to wait for a server to reboot before assuming something is wrong and aborting.  Line 3 is how long to wait after a server has successfully rebooted before checking for service status.  In other words, how much time should be given before checking that all Exchange services have started.  This is used along with the value for Line 4 to determine the total time to wait for services to start.  If any service isn’t started by the time the value for Line 3 has initially passed, the script will sleep for those number of minutes and then check again, repeating until all services are started or the number of checks in Line 4 have been reached.  Line 5 is the path and name of the log file to record the screen output to for later review.

The last part of the variables section is for notification.

#Mail settings
$mailSender = 'Server Reboot Script <srs@company.com>'
$mailServer = 'smtp.company.com' #SMTP FQDN to relay through
[array]$mailRecipients = 'user@company.com' #Recipients for notification and log file
$bSendStatusUpdates = $true #Set to false to not send status notification when each server is starting/ending
[array]$statusRecipients = 'phonenumber@SMSdomain.com' #Mobile recips for in-progress status updates

Lines 2 and 3 are self-explanatory.  Line 4 is a comma-separated list of addresses to send the script completion notice and transcript log.  It is cast as an array in case you specify only one recipient.  Line 5 indicates whether you want status updates to be sent, typically to a mobile device.  These updates are formatted with SMS recipients in mind, so they are to the point.  An status update is sent when a server is starting to be processed, when it has successfully rebooted and passes health checks, and when the script is complete or has aborted (including why it aborted).  This is so the person executing the script doesn’t have to keep an eye on the script to know its progress.  Line 6 is a comma-separated list of address to receive the status updates.  Again, it is cast as an array if you only have one recipient.  While it is geared for consumption by a mobile device via SMS, you can use any address you want.

This completes the variables section.  The next post will skip the functions section and delve into the script body, covering the functions as they are called.  Download the complete script:

  Reboot-ExchangeServers.zip (4.8 KiB)

Programmatically run Windows Update (as part of a broader patch and reboot process)

By , July 13, 2011 1:59 PM

It’s been awhile since I have posted a script, so this is the first post of my process for patching and rebooting the Exchange servers at work. I needed a way to patch the environment while maintaining high availability and not resorting to just staggering reboots at, say, 30-minute intervals. The staggered approach doesn’t account for any issues that can occur if a server doesn’t come back up or services fail to start, etc. What I have done to maintain HA starts with this script.

I originally intended to have the Windows Update process as part of my bigger patch-and-reboot script, but I learned that you can’t make calls to the Windows Update API from remote systems. This means that I need to execute the script locally on each server, but my reboot script runs remotely from a management server. Therefore, this is just a standalone script for installing software updates from Windows Update (or, in my case, Microsoft Update, since that is installed). It is locally installed as an on-demand Scheduled Task that gets remotely called from my reboot script, but I will go into those details when I post about that script.

The first function in the script is for writing to the event log. This is how my reboot script monitors the progress. I create a custom event source so that there is no chance of conflicting with anything else. Any time something is to be written I pass the message, type (Information, Warning, etc.), and event ID.

function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}

The search criteria is for software updates (no drivers) that are not installed and are checked by default. If you want to control which updates to install, you can manipulate the search criteria a little bit (as documented) so only certain updates are returned, but to not install an update associated with a specific KB article, you have to loop through the results and then not install the update that matches that property.

#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates

I use the WriteEvent function to write status updates to the event log so that my remote script can watch for events that indicate when it is done or that something has gone wrong. After the search results are downloaded, you still have to actually download them. Since I am not filtering any specific updates, I specify the entire $updates object to download.

#Create download object
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $updates
WriteEvent 'Beginning download of available updates.' 'Information' '1002'
$result= $downloader.Download()

When that is complete you loop through the collection and add them to a new installer object, which is finally used to installed the updates.

$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
#Create installer object
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $updatesToInstall
WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
#Run installation of downloaded files
$installationResult = $installer.Install()

Lastly, I loop through the results to log the name of every update that installed (or tried to install).

$global:counter=-1
$installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
	expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'

While you can have the script issue a reboot locally, I just log when the updates are complete so that my reboot script can trigger that an know precisely when that occurs. The entire script is below (since the code snippets above don’t include every line), but you can also download it.

  Run-WindowsUpdate.zip (1.2 KiB)

#Download and install software updates from WU that are selected by default
#v1.0 11/4/2010

function WriteEvent ($eventMessage,$eventType,$eventID)
	{
	$sourceName = 'UCTeam Scripts'
	if (-not([System.Diagnostics.EventLog]::SourceExists($sourceName)))
		{
		[System.Diagnostics.EventLog]::CreateEventSource($sourceName,'Application')
		}
	$EventLog = New-Object System.Diagnostics.EventLog('Application')
	$eventLogType = [System.Diagnostics.EventLogEntryType]::$eventType
	$EventLog.Source = $sourceName
	$EventLog.WriteEntry($eventMessage,$eventLogType,$EventID)
	}

#Software updates only, selected by default, not already installed
$criteria="IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1"
$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
$updateSession = New-Object -ComObject 'Microsoft.Update.Session'
WriteEvent 'Windows Update process is starting.' 'Information' '1000'
WriteEvent "Beginning check for available updates based on the following criteria: $criteria." 'Information' '1001'
$updates = $updateSession.CreateupdateSearcher().Search($criteria).Updates
if ($updates.Count -eq 0)
	{
	WriteEvent 'Check for available updates is complete.  There are no updates to apply.' 'Information' '1001'
	}
else
	{
	WriteEvent "Check for available updates is complete.  There are $($updates.Count) updates to apply." 'Information' '1001'
	#Create download object
	$downloader = $updateSession.CreateUpdateDownloader()
	$downloader.Updates = $Updates
	WriteEvent 'Beginning download of available updates.' 'Information' '1002'
	$result= $downloader.Download()
	if (($result.Hresult -eq 0) –and (($result.resultCode –eq 2) -or ($result.resultCode –eq 3)))
		{
		WriteEvent 'Download of available updates has completed.' 'Information' '1002'
		$updatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
		$updates | Where-Object {$_.isdownloaded} | Foreach-Object {$updatesToInstall.Add($_) | Out-Null}
		#Create installer object
		$installer = $updateSession.CreateUpdateInstaller()
		$installer.Updates = $updatesToInstall
		WriteEvent "Beginning installation of downloaded updates `($($installer.Updates.count)`)." 'Information' '1003'
        #Run installation of downloaded files
		$installationResult = $installer.Install()
        $global:counter=-1
        $installResults = $installer.updates | Select-Object -property Title,EulaAccepted,@{label='Result'; `
			expression={$resultCode[$installationResult.GetUpdateResult($($global:counter++)).resultCode]}}
		WriteEvent ($installResults | Format-Table -Wrap | Out-String) 'Information' '1002'
		}
	else
		{
		WriteEvent 'Error downloading updates.' 'Warning' '1001'
		}
	}
WriteEvent 'Windows Update process is complete.' 'Information' '1010'

#Reboot
#Restart-Computer -Force -Confirm:$false

Use the Exchange Management Shell or LDAP to get a list of quarantined mobile devices

By , June 6, 2011 8:23 AM

You can use Exchange Control Panel to view the list of Exchange ActiveSync devices that are in a quarantined state.  I wanted to be able to get this list without using ECP, but I didn’t know where this information is stored: Exchange or AD?  Long story short, it is stored in AD.  Every Exchange ActiveSync partnership exists as an AD object (whose class is msExchActiveSyncDevice) located as a child object of the user object whose mailbox has the partnership.  The access state of the device is stored as an integer in the msExchDeviceAccessState attribute of the object.

To use EMS to get the list, run this command:

Get-ActiveSyncDevice -filter {deviceaccessstate -eq 'quarantined'}

If you want to use LDAP to get the list, this is the corresponding search filter:

(&(objectclass=msexchactivesyncdevice)(msexchdeviceaccessstate=3))

The device access state values are 1 for allowed, 2 for blocked, 3 for quarantined.

Script to change security DL ownership

By , January 11, 2011 3:51 PM

Exchange 2010 implements a change to managing security distribution lists, especially in SP1. When using the Exchange Management Console (EMC) in the RTM version, changes made to membership or ownership of a security distribution list are successful when you are not the owner. This is because a parameter of the Set-DistributionGroup cmdlet is tacitly added that allows someone with the RBAC management role of Security Group Creation and Membership to update the DL. Starting with Exchange 2010 SP1, the console no longer adds this parameter (-BypassSecurityGroupManagerCheck).

The result is that, despite being an administrator (by being in the Organization Management role), if you are not explicitly listed as an owner of a security DL, you will not be able to makes changes to ownership or members via the console…period. This goes against the functionality that has existed in the Exchange console since 1996. And better yet, Microsoft says this is by design. The fact that it worked in RTM is the bug, not the other way around in SP1. I think that is one of the most boneheaded changes made in 2010. The solution is to either add the administrators as explicit owners or have them use the shell and manually add -BypassSecurityGroupManagerCheck.

Both options are impractical. If your help desk manages DLs you can’t just add all members as owners. For one thing, even though Exchange 2010 supports multiple owners, Outlook (even 2010) only displays the first entry. This means end users won’t know who to contact because the listed owners could be one or two “real” owners and all the rest are help desk. The other option, using the shell, isn’t the easiest for anyone that isn’t a dedicated Exchange admin.

My solution for the help desk staff is a PowerShell script that uses Windows forms to provide a GUI that lets you search for a DL, display the ownership, add and remove owners, then apply the changes. It includes username validation so it won’t let you add a user that isn’t mailbox-enabled.

The one place I struggled is connecting to Exchange. The script checks for the Exchange cmdlets being in memory. If they are not, it will connect remotely to a designated server. Because that is done when the form is activated, it causes the painting of the fields and labels to be delayed until the connection is complete. I tried to find a way to wait until the painting is complete before connecting, but you can’t detect that when the fields are drawn by Windows directly and not the application.

Change DL Owner screenshot

The code is listed here, but you can also download a zip file of it:

  Change-DLOwner.zip (3.3 KiB)

#Change ownership of Exchange 2010 SP1 distribution list
#Author: Scott Bueffel, http://www.flobee.net
#v1.0 1/6/10

$connectToServer = 'ServerName' #Server to connect to if shell doesn't have cmdlets in memory
$DLPrefix = '[DL] ' #Display name standard prefix.  If none, leave as two single-quotes

function ConnectToExchange
	{
	$testcmd = gcm Get-Mailbox -ErrorAction SilentlyContinue
	if (-not($testcmd))
		{
		$statusBarPanel1.Text = 'Connecting to Exchange...'
		#Connect remotely to Exchange
		$global:exsession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$connectToServer/PowerShell" -Name exchange
		Import-PSSession $exsession -AllowClobber -DisableNameChecking | Out-Null
		}
	$statusBarPanel1.Text = 'Connected to Exchange'
	}

#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
#endregion

#region Generated Form Objects
$form = New-Object System.Windows.Forms.Form
$txtAddOwner = New-Object System.Windows.Forms.TextBox
$label7 = New-Object System.Windows.Forms.Label
$label6 = New-Object System.Windows.Forms.Label
$btnRemoveOwner = New-Object System.Windows.Forms.Button
$btnAddOwner = New-Object System.Windows.Forms.Button
$lstOwners = New-Object System.Windows.Forms.ListBox
$label2 = New-Object System.Windows.Forms.Label
$label1 = New-Object System.Windows.Forms.Label
$btnGetOwner = New-Object System.Windows.Forms.Button
$statusBar1 = New-Object System.Windows.Forms.StatusBar
$btnCancel = New-Object System.Windows.Forms.Button
$btnApply = New-Object System.Windows.Forms.Button
$label5 = New-Object System.Windows.Forms.Label
$txtDLName = New-Object System.Windows.Forms.TextBox
$label4 = New-Object System.Windows.Forms.Label
$label3 = New-Object System.Windows.Forms.Label
$toolTip1 = New-Object System.Windows.Forms.ToolTip
$statusBarPanel1 = New-Object System.Windows.Forms.StatusBarPanel
$statusBarPanel2 = New-Object System.Windows.Forms.StatusBarPanel
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
$errorprovider = New-Object System.Windows.Forms.ErrorProvider
#endregion Generated Form Objects

function DisplayOwners ($DL)
	{
	$lstOwners.BeginUpdate()
	[array]$DLOwners = $DL.ManagedBy
	foreach ($owner in $DLOwners)
		{
		$lstOwners.Items.Add($owner.ToString().Substring($owner.ToString().LastIndexOf('/') + 1))
		}
	$lstOwners.EndUpdate()
	$txtAddOwner.Enabled = $true
	}

function RetrieveDL
	{
	$lstOwners.Items.Clear() #Remove any entries in owner list from previous query
	$btnAddOwner.Enabled = $false
	$txtAddOwner.Enabled = $false
	$statusBarPanel2.Text = 'Searching for DL...'
	$DLDisplayName = "$($DLPrefix)$($txtDLName.Text)"
	$script:DL = Get-DistributionGroup $DLDisplayName -ErrorAction SilentlyContinue
	if (!($DL))
		{
    	$errorProvider.SetError($txtDLName, 'The DL cannot be found.')
		$statusBarPanel2.Text = 'DL not found.'
    	return
  		}
  	#Valid DL returned
  	$errorProvider.SetError($txtDLName, '')
	DisplayOwners $DL
	$statusBarPanel2.Text = ''
	$btnCancel.Text = 'Cancel'
	}

function AddOwner_OnClick ($username)
	{
	$statusBarPanel2.Text = 'Validating user...'
	$ownerMailbox = Get-Mailbox $username -ErrorAction SilentlyContinue
	if (!($ownerMailbox))
		{
		$errorprovider.SetIconAlignment($txtAddOwner,2)
		$errorprovider.SetError($txtAddOwner, 'The user cannot be found.')
		$statusBarPanel2.Text = 'User not found.'
		return
		}
	elseif ($lstOwners.Items.Contains($username))
		{
		$errorprovider.SetIconAlignment($txtAddOwner,2)
		$errorprovider.SetError($txtAddOwner, 'The user is already in the list.')
		$statusBarPanel2.Text = 'User already is owner.'
		return
		}
	#Valid user found
	$lstOwners.Items.Insert(0,$username) #Add owner to top of list
	$statusBarPanel2.Text = 'User added.'
	$errorprovider.SetIconAlignment($txtAddOwner,3)
	$errorprovider.SetError($txtAddOwner,'')
	$txtAddOwner.Text = ''
	$btnApply.Enabled = $true
	}

function ToggleAddButton ($username)
	{
	if ($username.Length -gt 0)
		{
		$btnAddOwner.Enabled = $true
		}
	else
		{
		$btnAddOwner.Enabled = $false
		}
	}

function RemoveOwner_OnClick ($owner)
	{
	$lstOwners.Items.Remove($owner)
	$btnRemoveOwner.Enabled = $false
	$btnApply.Enabled = $true
	}

function ApplyChanges_OnClick
	{
	[array]$newOwners = $lstOwners.Items
	try
		{
		Set-DistributionGroup $($DL.DisplayName) -ManagedBy $newOwners -BypassSecurityGroupManagerCheck -ErrorAction Stop
		$statusBarPanel2.Text = 'DL ownership applied.'
		$errorprovider.SetError($btnApply, '')
		$btnCancel.Text = 'Close'
		$btnApply.Enabled = $false
		}
	catch
		{
		$statusBarPanel2.Text = 'Error occurred updating DL.'
		$errorprovider.SetError($btnApply, "Error occurred when updating the DL's new ownership.")
		}
	}

$OnLoadForm_StateCorrection = {$form.WindowState = $InitialFormWindowState}

#region Generated Form Code
$form.CancelButton = $btnCancel
$form.Text = "Change Distribution List Owner"
$form.Name = "form"
$form.KeyPreview = $True
$form.StartPosition = 1
$form.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 342
$System_Drawing_Size.Height = 363
$form.ClientSize = $System_Drawing_Size

$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 84
$System_Drawing_Size.Height = 20
$txtAddOwner.Size = $System_Drawing_Size
$txtAddOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$txtAddOwner.Text = "Enter username"
$txtAddOwner.Name = "txtAddOwner"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 211
$txtAddOwner.Location = $System_Drawing_Point
$txtAddOwner.Enabled = $False
$txtAddOwner.TabIndex = 15
$txtAddOwner.add_Enter({if ($txtAddOwner.Text -eq 'Enter username') {$txtAddOwner.Text = ''}})
$txtAddOwner.add_KeyUp({ToggleAddButton $txtAddOwner.Text})
$txtAddOwner.add_KeyDown({if ($_.KeyCode -eq "Enter"){AddOwner_OnClick $txtAddOwner.Text}})

$form.Controls.Add($txtAddOwner)

$label7.TabIndex = 12
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 20
$System_Drawing_Size.Height = 23
$label7.Size = $System_Drawing_Size
$label7.Text = "2."
$label7.Font = New-Object System.Drawing.Font("Arial",9,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 13
$System_Drawing_Point.Y = 108
$label7.Location = $System_Drawing_Point
$label7.DataBindings.DefaultDataSourceUpdateMode = 0
$label7.Name = "label7"

$form.Controls.Add($label7)

$label6.TabIndex = 13
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 283
$System_Drawing_Size.Height = 35
$label6.Size = $System_Drawing_Size
$label6.Text = "Add and remove owners as necessary, then click Apply Changes to update the DL's ownership."
$label6.Font = New-Object System.Drawing.Font("Arial",8,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 109
$label6.Location = $System_Drawing_Point
$label6.DataBindings.DefaultDataSourceUpdateMode = 0
$label6.Name = "label6"

$form.Controls.Add($label6)

$btnRemoveOwner.TabIndex = 7
$btnRemoveOwner.Name = "btnRemoveOwner"
$btnRemoveOwner.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnRemoveOwner.Size = $System_Drawing_Size
$btnRemoveOwner.UseVisualStyleBackColor = $True

$btnRemoveOwner.Text = "Remove"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 252
$btnRemoveOwner.Location = $System_Drawing_Point
$btnRemoveOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnRemoveOwner.add_Click({RemoveOwner_OnClick $lstOwners.SelectedItem})

$form.Controls.Add($btnRemoveOwner)

$btnAddOwner.TabIndex = 6
$btnAddOwner.Name = "btnAddOwner"
$btnAddOwner.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnAddOwner.Size = $System_Drawing_Size
$btnAddOwner.UseVisualStyleBackColor = $True

$btnAddOwner.Text = "Add"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 181
$btnAddOwner.Location = $System_Drawing_Point
$btnAddOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnAddOwner.add_Click({AddOwner_OnClick $txtAddOwner.Text})

$form.Controls.Add($btnAddOwner)

$lstOwners.FormattingEnabled = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 141
$System_Drawing_Size.Height = 134
$lstOwners.Size = $System_Drawing_Size
$lstOwners.DataBindings.DefaultDataSourceUpdateMode = 0
$lstOwners.Name = "lstOwners"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 155
$lstOwners.Location = $System_Drawing_Point
$lstOwners.TabIndex = 5
$lstOwners.add_SelectedIndexChanged({$btnRemoveOwner.Enabled = $true})

$form.Controls.Add($lstOwners)

$label2.TabIndex = 10
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 21
$System_Drawing_Size.Height = 23
$label2.Size = $System_Drawing_Size
$label2.Text = "1."
$label2.Font = New-Object System.Drawing.Font("Arial",9,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 12
$label2.Location = $System_Drawing_Point
$label2.DataBindings.DefaultDataSourceUpdateMode = 0
$label2.Name = "label2"

$form.Controls.Add($label2)

$label1.TabIndex = 11
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 288
$System_Drawing_Size.Height = 32
$label1.Size = $System_Drawing_Size
$label1.Text = "Enter the display name of the DL and click Get Owners to display the ownership."
$label1.Font = New-Object System.Drawing.Font("Arial",8,0,3,0)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 13
$label1.Location = $System_Drawing_Point
$label1.DataBindings.DefaultDataSourceUpdateMode = 0
$label1.Name = "label1"

$form.Controls.Add($label1)

$btnGetOwner.TabIndex = 3
$btnGetOwner.Name = "btnGetOwner"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnGetOwner.Size = $System_Drawing_Size
$btnGetOwner.UseVisualStyleBackColor = $True

$btnGetOwner.Text = "Get Owners"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 77
$btnGetOwner.Location = $System_Drawing_Point
$btnGetOwner.DataBindings.DefaultDataSourceUpdateMode = 0
$btnGetOwner.add_Click({RetrieveDL})
$form.Controls.Add($btnGetOwner)

$statusBar1.ShowPanels = $True
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 342
$System_Drawing_Size.Height = 22
$statusBar1.Size = $System_Drawing_Size
$statusBar1.TabIndex = 14
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 0
$System_Drawing_Point.Y = 341
$statusBar1.Location = $System_Drawing_Point
$statusBar1.DataBindings.DefaultDataSourceUpdateMode = 0
$statusBar1.Name = "statusBar1"
$statusBar1.Panels.Add($statusBarPanel1)|Out-Null
$statusBar1.Panels.Add($statusBarPanel2)|Out-Null

$form.Controls.Add($statusBar1)

$btnCancel.TabIndex = 9
$btnCancel.Name = "btnCancel"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 75
$System_Drawing_Size.Height = 23
$btnCancel.Size = $System_Drawing_Size
$btnCancel.UseVisualStyleBackColor = $True

$btnCancel.Text = "Cancel"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 198
$System_Drawing_Point.Y = 302
$btnCancel.Location = $System_Drawing_Point
$btnCancel.DataBindings.DefaultDataSourceUpdateMode = 0
$btnCancel.DialogResult = 2

$form.Controls.Add($btnCancel)

$btnApply.TabIndex = 8
$btnApply.AutoSize = $True
$btnApply.Name = "btnApply"
$btnApply.Enabled = $False
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 88
$System_Drawing_Size.Height = 23
$btnApply.Size = $System_Drawing_Size
$btnApply.UseVisualStyleBackColor = $True

$btnApply.Text = "Apply Changes"

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 60
$System_Drawing_Point.Y = 302
$btnApply.Location = $System_Drawing_Point
$btnApply.DataBindings.DefaultDataSourceUpdateMode = 0
$btnApply.add_Click({ApplyChanges_OnClick})

$form.Controls.Add($btnApply)

$label5.TabIndex = 4
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 66
$System_Drawing_Size.Height = 23
$label5.Size = $System_Drawing_Size
$label5.Text = "Owner(s):"
$label5.Font = New-Object System.Drawing.Font("Arial",9,1,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 35
$System_Drawing_Point.Y = 156
$label5.Location = $System_Drawing_Point
$label5.DataBindings.DefaultDataSourceUpdateMode = 0
$label5.Name = "label5"

$form.Controls.Add($label5)

$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 198
$System_Drawing_Size.Height = 20
$txtDLName.Size = $System_Drawing_Size
$txtDLName.DataBindings.DefaultDataSourceUpdateMode = 0
$txtDLName.MaxLength = 255
$txtDLName.Name = "txtDLName"
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 120
$System_Drawing_Point.Y = 48
$txtDLName.Location = $System_Drawing_Point
$txtDLName.TabIndex = 2
$txtDLName.add_KeyDown({if ($_.KeyCode -eq "Enter"){RetrieveDL}})

$form.Controls.Add($txtDLName)

$label4.TabIndex = 1
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 103
$System_Drawing_Size.Height = 23
$label4.Size = $System_Drawing_Size
$label4.Text = "DL Name:  $DLPrefix"
$label4.Font = New-Object System.Drawing.Font("Arial",9,1,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 30
$System_Drawing_Point.Y = 51
$label4.Location = $System_Drawing_Point
$label4.DataBindings.DefaultDataSourceUpdateMode = 0
$label4.Name = "label4"

$form.Controls.Add($label4)

$label3.TabIndex = 2
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 10
$System_Drawing_Size.Height = 23
$label3.Size = $System_Drawing_Size
$label3.Text = "."

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 138
$System_Drawing_Point.Y = 13
$label3.Location = $System_Drawing_Point
$label3.DataBindings.DefaultDataSourceUpdateMode = 0
$label3.Name = "label3"

$form.Controls.Add($label3)

$toolTip1.ShowAlways = $True
$toolTip1.IsBalloon = $True

$statusBarPanel1.Name = "statusBarPanel1"
$statusBarPanel1.Text = "Connecting to Exchange..."
$statusBarPanel1.AutoSize = 2
$statusBarPanel1.Width = 162

$statusBarPanel2.Name = "statusBarPanel2"
$statusBarPanel2.Text = ""
$statusBarPanel2.AutoSize = 2
$statusBarPanel2.Width = 162
$statusBarPanel2.BorderStyle = 2

$toolTip1.ShowAlways = $True
$toolTip1.IsBalloon = $True
$toolTip1.AutomaticDelay = 250
$toolTip1.AutoPopDelay = 5000
$toolTip1.ToolTipIcon = [System.Windows.Forms.ToolTipIcon]::Info
$txtOwnersTip = "Enter the username of someone to be added as an owner."
$txtDLNameTip = "The display name of the DL as it appears in the address book."
$toolTip1.SetToolTip($txtDLName,$txtDLNameTip)
$toolTip1.SetToolTip($txtAddOwner,$txtOwnersTip)

#endregion Generated Form Code

$OnLoadForm_StateCorrection = {$form.WindowState = $InitialFormWindowState}
$InitialFormWindowState = $form.WindowState
$form.add_Load($OnLoadForm_StateCorrection)
$form.add_Shown({$form.Activate();ConnectToExchange}) #Action when form is displayed
#Show the Form

[void] $form.ShowDialog()

Found a bug with Set-MailboxFolderPermission

By , December 9, 2010 1:39 PM

While working with conference room calendars at my company, I set the Default user permission to Reviewer so that anyone can open the calendar to see meetings in a layout that is far easier to work with than the scheduling assistant. If you set the permission with Outlook, everything works well. If you use EMS to do it with Set-MailboxFolderPermission (Exchange 2010 SP1), it runs successfully, but not everything works well. The main thing I have noticed is that users can’t print the calendar.

I used MAPI Editor (née MFCMAPI) to see what is different when using Outlook versus PowerShell. The permission set on the Calendar folder is the same with both methods, so I suspected it had something to do with the Freebusy Data folder. I charted what permission is set on this folder with both methods with different users, and also adding a user and changing an existing one.

What I have found is that both Add-MailboxFolderPermission and Set-MailboxFolderPermission work correctly when applying permissions for named users. Setting any permission on the calendar results in the person being granted Editor permission to the Freebusy Data folder. (If you remove the permission on the calendar, the Editor permission remains on the Freebusy Data folder, which isn’t a problem.)

There is an issue, however, when setting the permission for the Default user entry which, of course, is a special user that simply represents any user who isn’t explicitly listed in the ACL. Permission on the Freebusy Data folder is not updated when setting the permission for the Default user entry on the Calendar folder, leaving the entry with a bitmask value of 0 (no rights) as seen in MAPI Editor.

To work around this issue, I looked into using Exchange Web Services and also the EWS Managed API. I ran into problems enumerating the ACL on the Freebusy Data folder (it is listed as empty). While trying to resolve that I ended up finding a much simpler solution which doesn’t need anything more than Set-MailboxFolderPermission. You can access the Non_IPM_Subtree with the cmdlet just by including it as part of the path. The workaround is to manually set the permission on the Freebusy Data folder after you do it on Calendar. This is only necessary when setting the permission for the Default user entry.

Set-MailboxFolderPermission ‘MB Identity:\calendar’ -User default -AccessRights reviewer
Set-MailboxFolderPermission ‘MB Identity:\non_ipm_subtree\freebusy data’ -User default -AccessRights reviewer

New version of LDAP formatting script

By , November 12, 2010 10:39 AM

Over two years ago, I posted a script (in VBScript) that formats an LDAP filter for readability and outputs it in an IE window. I seldom use VBScript anymore since Exchange is all about the PowerShell. I still work with LDAP filters all the time to control membership in automated DLs (Exchange’s query-based DLs are very limited), so I was working in PowerShell to do that and still hopping over to my old script to format it for myself or others.

I thought it was time to update that script to work in PowerShell. While some things port from VBScript to PowerShell quite easily, I got stuck in several places doing this one. Working with the InternetExplorer.Application COM object is one, but researching the changes allowed me to improve the formatting. The font is smaller and I have added pipes to show where the indenting occurs, which helps with the flow when the filter is long and you have to scroll.

A sticking point was working with the search and replace. You can’t use the parameters for the starting position and the number of replacements to make when using the replace operator in PowerShell. You can, however, do it with the RegularExpressions class (RegEx), but then I had to accommodate the special characters in the regex world. That was offset by the lack of a need to escape as many characters in PowerShell as in VBScript.

The main issue, though, was interpreting my old code. It is documented well, but I could not figure out why I was doing certain things to manipulate the filter as the indentations were being added and keeping track of the correct position in the characters. I spent a fair number of hours trying to figure out if certain lines were just superfluous or were actually needed for it to work. I was able to remove lines of code that were at least not needed in PowerShell, though I still don’t know why I used them in them VBScript version.

In the end, the results are the same as the old version except for the formatting changes (which are an improvement). I have thought about changing the formatted filter to XML so that each indentation can be collapsed when viewing in IE, but that is more work than needed at this time. The full code is below, but you can also just download the script via the link at the end. (I have also included the old VBScript version in the download.)

#Takes an LDAP filter from the clipboard and formats it for readability
#v1.0 11/12/10
#http://www.flobee.net

function GetClipBoard
	{
    Add-Type -AssemblyName System.Windows.Forms
    $tb = New-Object System.Windows.Forms.TextBox
    $tb.Multiline = $true
    $tb.Paste()
    $tb.Text
	}

function FormatLDAPDisplay($infilter)
	{
	#Replace pipes with crosshatches to keep them from interfering in regex
	$infilter = $infilter.Replace('|','#')
	#Iterate through each character in filter and insert line breaks and nesting
	$iPos = 0
	$iIndentCount = 0
	while ($iPos + 1 -lt $infilter.Length)
		{
		$currCharacter = $infilter.Substring($iPos, 1)
		$iWhileIndent = 1
		$sIndentation = ''
		switch -regex ($currCharacter)
			{
			#LDAP operators to watch for to modify nesting level.
			#NOT operator ignored because only used in one-off attribute value
			"[&#]"
				{
				#Operator followed by open paren means nesting increase
				if ($infilter.Substring($iPos + 1, 1) -eq '(')
					{
					$iIndentCount ++
					#Build nest based on number of indentations
					while ($iWhileIndent -le $iIndentCount)
						{
						#Use unique string as placeholder for nesting with HTML spaces
						$sIndentation += '|QZNBSPQZNBSPQZNBSPQZNBSP'
						$iWhileIndent ++
						}
					#Insert new string for formatting
					$regx = [regex]$currCharacter
					$infilter = $regx.Replace($infilter, "$currCharacter$sIndentation", 1, $iPos)
					#Move current position to next character after inserted string
					$iPos += $sIndentation.Length + 5
					}
				else
					{
					$iPos ++
					}
				}
			"[\(]" #Escape paren to be treated literally in regex
				{
				while ($iWhileIndent -le $iIndentCount)
					{
					$sIndentation += "|QZNBSPQZNBSPQZNBSPQZNBSP"
					$iWhileIndent ++
					}
				if ($sIndentation -ne '')
					{
					if ($iPos -ne 0)
						{
						#If open paren follows close paren, insert line break
						if ($infilter.Substring($iPos - 1, 1) -eq ')')
							{
							$sIndentation += ''
							$regx = [regex]'\('
							$sNewLDAPFilter = $regx.Replace($infilter, "$sIndentation$currCharacter", 1, $iPos)
							$infilter = $infilter.Substring(0, $iPos - 1) + $sNewLDAPFilter
							$iPos += $sIndentation.Length + 1
							}
						else
							{
							$iPos ++
							}
						}
					else
						{
						$iPos ++
						}
					}
				else
					{
					$iPos ++
					}
				}
			"[\)]" #Escape paren to be treated literally in regex
				{
				#Two consecutive close paren means nesting reduces one level
				if ($infilter.Substring($iPos + 1, 1) -eq ')')
					{
					$iIndentCount --
					$bDblClose = $true
					}
				while ($iWhileIndent -le $iIndentCount)
					{
					$sIndentation += '|QZNBSPQZNBSPQZNBSPQZNBSP'
					$iWhileIndent ++
					}
				$regx = [regex]'\)'
				$infilter = $regx.Replace($infilter, "$currCharacter$sIndentation", 1, $iPos)
				#Adjust position to account for two close paren
				if ($bDblClose)
					{
					$iPos += $sIndentation.Length + 5
					$bDblClose = $null
					}
				else
					{
					$iPos += $sIndentation.Length + 6
					}
				}
			default
				{
				#No paren or operator means move to next character
				$iPos ++
				}
			}
		}

	#Replace LDAP operators with words
	$infilter = $infilter -replace '&', 'AND'
	$infilter = $infilter -replace '#', 'OR'
	$infilter = $infilter -replace '!', 'NOT '

	#'Replace spaceholders with HTML spaces
	$infilter = $infilter -replace 'QZNBSP', ' '

	$infilter
	}

$string = GetClipboard
$htmlbody = FormatLDAPDisplay $string

#Launch IE window
$ie = New-Object -ComObject "InternetExplorer.Application"
$ie.AddressBar = $false
$ie.Menubar = $false
$ie.Toolbar = $false
$ie.Resizable = $true
$ie.Height = 450
$ie.Width = 500
$ie.Visible = $true
$ie.Navigate('about:blank')
do
	{
  	Sleep -Milliseconds 100
	}
while ($ie.Busy)

$doc = $ie.Document
$doc.title = "Formatted LDAP Filter"
$doc.body.style.fontSize = '10pt'
$doc.body.innerHTML = $htmlbody

  Format-LDAPFilter.zip (3.0 KiB)

Exchange 2010 prerequisite installation script updated

By , August 24, 2010 11:37 AM

The installation script, originally referenced here, has been updated to version 1.5. It adds support for installing just the management tools, including on Windows 7. I was in the process of making it work on Vista as well, but it got messy because dism doesn’t come with Vista and I didn’t want to have separate installation routines for Windows 7 and Vista. So I am making the assumption that anyone who wants to install the management tools on a workstation has long given up on Vista.

To detect the OS (which is actually difficult and not uniform by any means) and make sure it is 64-bit, this sections is added:

# Detect correct OS here and exit if no match
$wmiOS = Get-WMIObject win32_OperatingSystem
$OScap = $wmiOS.Caption
$OSver = $wmiOS.Version
[array]$wmiProc = Get-WmiObject win32_Processor
if ($wmiProc[0].Architecture -eq '9')
	{
	if ($OScap -match 'Windows 7')
		{$os = 'Win7'}
	elseif (($OSver -eq '6.1.7600') -and ($OScap -match '2008'))
		{$os = 'R2'}
	elseif ($OSver -eq '6.0.6002')
		{$os = 'R1'}
	else
...

Getting the processor as an array makes it work with both single and multi-processors. As a reader kindly pointed out, when run on an R2 server, it kept thinking WinRM wasn’t installed. This is because WinRM is preinstalled on R2 so the check for the hotfix KB installation will always fail. It now skips the WinRM check for an R2 server.

I also updated the menu so you can select the management tools, and made it so you only are given the option of selecting the management tools when run on Windows 7. (Only the menu reflects this restriction. If you select a different option the script will still try and run that command.) The download link is below.

  Prepare-Exchange2010Install.zip (4.7 KiB)

Automatically disable ActiveSync for new mailboxes in Exchange 2010

By , June 30, 2010 2:51 PM

One of the new features in Exchange 2010 is the use of cmdlet extension agents, as described in this post. Using the Scripting Agent you can have Exchange ActiveSync disabled whenever a mailbox is created for a new or existing user. This removes the need to do it directly against Active Directory through some workflow mechanism or scheduling a task to run that does it with the Set-CASMailbox cmdlet.

There is almost no documentation on the use of the provisioning handler for Exchange 2010, leaving me to do a lot of trial and error to get it working for new mailboxes for both new and existing users. It doesn’t look like the provisioning handler has access to any of the information returned by the success of the New-Mailbox and Enable-Mailbox cmdlets. This means it only has access to the information submitted by the user in a cmdlet. Because you supply different information when creating a mailbox for a new user compared to an existing one, the code has to be different for each.

Copy the code below into the ScriptingAgentConfig.xml file and, as Pat Richard’s post details, put it in the CmdletExtensionAgents directory and enable the Scripting Agent.

<?xml version="1.0" encoding="utf-8" ?>
<Configuration version="1.0">
	<Feature Name="MailboxProvisioning" Cmdlets="enable-mailbox">
		<ApiCall Name="OnComplete">
			if($succeeded)
				{
				$user = (Get-User $provisioningHandler.UserSpecifiedParameters["Identity"]).distinguishedName
				Set-CASMailbox $user -ActiveSyncEnabled $false
				}
		</ApiCall>
	</Feature>
	<Feature Name="MailboxProvisioning" Cmdlets="new-mailbox">
		<ApiCall Name="OnComplete">
			if($succeeded)
				{
				$user = (Get-User $provisioningHandler.UserSpecifiedParameters["Name"]).distinguishedName
				Set-CASMailbox $user -ActiveSyncEnabled $false
				}
		</ApiCall>
	</Feature>
</Configuration>

Panorama Theme by Themocracy