Category: Exchange 2010

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)

Updated script to install Exchange 2010 prerequisites

By , December 14, 2011 8:57 AM

I have updated the script, first discussed in this post, then updated here and here, to account for new hotfixes since I last updated it.  The following is a list of changes:

  • The list of required OS hotfixes is based on this TechNet page.
  • The OS version code is less restrictive, the better to handle minor version updates that don’t change the fact that a server is R1 or R2.
  • The list of files to install uses hash tables instead of simple arrays.
  • Most of the hotfixes added require manually downloading because they require accepting a EULA or are only available through Premier or manual request from the KB article.  If one of these is not already downloaded, the script output will give the URL and then exit.  Even though the script will download missing hotfixes for you when applicable, the process will be smoother for you if you download the manual ones ahead of time.
  • Only the hotfixes for the specific version of Windows (R1, R2, Win7) are installed.  The TechNet page lists two hotfixes for Windows 7, but neither are installed on my workstation and they report they are not applicable when I attempt to install them.  Others report similar results, so I have remarked those two out.
  • Installing on R1 no longer requires the Exchange-supplied XML files for the OS features.  Like R2, the features to install are contained within the script.
  • Exchange 2010 SP2 requires that CAS has the “IIS WMI Compatibility” feature installed, so that has been added to the appropriate role selection.
  • Not listed on the TechNet page, but required for the UM role are the Unified Communications Managed API (version 2) and the Speech Server Platform Runtime.  Selecting the UM option will include installation of these.  The UC Managed API install uses a wizard because of ancillary programs that it may need to download (such as VC++ 2008 runtime), so it doesn’t support a silent install.
  • Other minor updates.

I have done limited testing with updated version, but please let me know if you have any issues so I can address them.

  Prepare-Exchange2010Install.zip (4.7 KiB)

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

By , December 7, 2011 2:29 PM

This post is part of a multi-part series:

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

Back in July, I posted a script that uses Microsoft Update to download and install hotfixes. That script is part of a bigger process that I use to patch all of my Exchange servers. I mentioned that the script for that process was forthcoming. It is a big script, and I have yet to post it because I am always tweaking it. But if I wait for it to be code complete, I will never post it. So this is the first post of what will be several to document what it does.  This post will only cover the environment in which I am working and what the script requirements are.

First is the environment.  It is wholly Exchange 2010 (as of this writing, all servers are SP1 RU6) on Windows 2008 SP2.  There are over 8000 mailboxes, of which 1500 or so are shared.  There are no public folders.  Server and database infrastructure:

  • Two data centers, one for production and one effectively for DR
  • Primary data center:
    • 2 HT/CAS servers behind a hardware load balancer (HLB)
    • 2 UM servers
    • 3 active MB servers
    • 1 lag MB server
  • Secondary data center (DR):
    • 2 HT/CAS servers behind an HLB
    • 1 UM server
    • 2 MB servers
  • DAG properties
    • Single DAG that spans both sites
    • 22 databases
    • 2 copies of each database across the 3 mailbox servers in the primary data center, e.g., DB1 is on server 1 and 2, DB2 is on server 2 and 3, etc.
    • All databases have a copy on the lag server
    • Each mailbox server in the secondary data center has a copy of every database
    • Each database has 5 copies total (including lag)

At my company, patch management is done by an application that has no awareness of Exchange, and reboots are simply staggered.  This is not an acceptable solution for patching an Exchange environment that has high availability, needs databases to be balanced after patching, and ideally best practices are followed for suspending DB replication before rebooting, etc.  As a result, these are the requirements for automated patching and rebooting of my Exchange environment:

  • Process each server serially so that any problem that arises never affects more than one server, maintaining HA
  • Move active copies of databases to other servers so there is zero impact to clients
  • Suspend and resume replication of passive copies before and after reboots
  • Balance active databases when all mailbox servers are complete
  • Put servers in maintenance mode
  • Send status notifications to mobile device
  • Send detailed log when complete
  • Gracefully exit if any timeouts are reached (patching, rebooting, service starting, etc.)
  • Verify required services are started before moving on

My script, currently at 445 lines, meets all of these requirements.  There are other things I intend to add to it, but I will go into detail after I have deconstructed the script for you in future posts.  Part 2 of this series, which will cover the overall script structure and its first section (variables), will be published soon.

Download the complete script:

  Reboot-ExchangeServers.zip (4.8 KiB)

Super duper delegate retrieval script

By , October 7, 2011 10:56 AM

Update 11/11/11: Minor update made to resolve a potential issue when translating user name to SID. Inline code and download have been updated.

Getting delegate information has always been tricky. Information is stored in multiple places, you have to use different tools to get that information, and business requirements can add to the data needed to paint a complete picture for a given delegate. Taking advantage of the EWS Managed API to do the heavy lifting for information not easily exposed, I wrote this script to report delegate permissions and settings, mostly to help when troubleshooting why a delegate can’t do something or is getting weird behavior.

Using the GetDelegates() method of the managed API not only retrieves the delegates and their permissions to the well-known folders, but also whether private items are visible, if meeting requests are sent to a specific delegate, and the meeting request handling of the owner. In reality, though, that isn’t enough information to give the total picture. Full mailbox access supersedes folder permissions, so knowing who has that is needed. My company uses the registry modifications that control where deleted items and sent items, which means a delegate needs permission to those folders, so getting that is needed. Lastly, delegates are granted send on behalf of permission (which, incidentally, isn’t even required for a delegate to send a meeting request on behalf of the owner), but if a delegate has send as permission, that takes precedence when sending email messages, so knowing if a delegate has that right is also necessary.

The only required argument is an owner’s identity, such as email address, username, alias, etc. An optional switch parameter is -includeSendAs. I made it an opt-in feature because getting the send as information from AD makes the entire script run over 12 times longer (in my company’s infrastructure): 1.7 seconds versus 20.7 seconds for an owner with five delegates. Although I prefer to not hard-code information that can be determined dynamically, I took the poor man’s route for the EWS URL, so you need to enter that on line 26. If you don’t care about any of the details of the script contents, you can simply stop reading, download it, and run with it.

  Get-Delegates.zip (2.3 KiB)

Thanks to the managed API, retrieving the delegates can be done in just six lines (four, if you specify the DLL path when loading it instead of putting it in a variable, and the same for setting the EWS URL).

$ewsuri=[system.URI]"https://owa.domain.com/ews/exchange.asmx"
$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
$service.Url = $ewsuri
$service.GetDelegates($owner,$true)

After getting the delegates, full mailbox access and, optionally, send as are retrieved. Because of the added time to get send as, I added a progress bar while information is being retrieved. The delegate properties stored in the owner’s mailbox include email address, display name, and SID. The property returned for full mailbox access and send as is username. Since there isn’t a common property between the two collections, the usernames must be converted to SIDs:

function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User)
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}

Because of the registry changes to store items deleted from the owner’s mailbox in the owner’s Deleted Items folder, and to store items sent by the delegate from the owner in the owner’s Sent Items folder, Author permission or higher is needed for those folders. This is easy to do with Get-MailboxFolderPermission.

Once all the information has been retrieved, you need to loop through the collection of delegates. When an orphaned delegate is on an owner’s mailbox, Exchange returns the delegate in the collection, but only as an error, not with any identifying information. I look for a matching error message and then direct you to the hidden message and property that lists all the delegates to find out which delegate is orphaned:

$delegates.DelegateUserResponses | ForEach {
	if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
		{
		Write-Output "*Orphaned Delegate"
		Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
		Write-Output "  property 0x684A101E to determine orphan entry."
		}

To report whether a given delegate has full mailbox access or send as permission, the SID of the delegate is matched against the respective array from the GetSID() function above:

if ($fmaSID -match $_.delegateuser.UserId.SID)
	{
	Write-Output "  Mailbox-level permission: Full Mailbox Access"
	}

The collection includes the granted permission to the well-known folders exposed in Outlook’s delegation wizard. Since it is rare for someone to use or grant permission to Journal or Notes, rather than just echo the permission array, I display just the other four folders:

			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"

To include the delegate’s potential permission to Deleted Items and Sent Items, I pass the respective folder permission object to the pipeline looking for a display name match:

[array]$delegateDIPerm = $deletedItemsPerm | where {$_.User -eq $delegateDisplayName}
if ($delegateDIPerm.Count -eq 1)
	{
	Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
	}
else
	{
	Write-Output "    Deleted Items: None"
	}

Lastly, whether a delegate receives meeting requests or can view private items is a property of the individual delegate’s entry in the collection. How the owner wants meeting requests handled is not per delegate, so that setting is a property of the root collection. All of this is then output to the success stream, whether that is the screen (the default) or redirection to a file.

You can download the script from the link above, or copy the contents below (double-click in code area to highlight all).

<#
.Synopsis
	Display a mailbox's delegates and permissions.
.Description
	Retrieve the list of delegates for a mailbox and display the mailbox permission,
	folder permissions, meeting invite settings, and (optionally) whether the
	delegate has Send As permission.
.Parameter MailboxOwner
	Valid identity string of the user whose mailbox has the delegates.
.Parameter IncludeSendAs
	Switch to indicate that you want Send As permission to be included.
.Example
	get-delegates.ps1 user@domain.com -includesendas
.Example
	get-delegates.ps1 domain\username
.Notes
	Version: 1.21
	Date: 11/9/11
#>
Param (
	[Parameter(Position = 0,Mandatory = $true,HelpMessage="Identity of mailbox owner")][string]$mailboxOwner,
	[Alias("SA")][switch]$includeSendAs #Perform Send As lookup (takes longer)
	)

#Region Variables
$ewsuri=[system.URI]"https://owa.domain.com/ews/exchange.asmx" #EWS URL
#EndRegion

#Region Functions
function LoadAPI
	{
	$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll"
	if (-not(Test-Path $dllpath))
		{
		Write-Output "This system does not have the EWS Managed API installed, which is required to run this script."
		Write-Output "The API can be download at http://www.microsoft.com/download/en/details.aspx?id=13480"
		exit
		}
	[void][Reflection.Assembly]::LoadFile($dllpath)
	}

function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User)
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}

function GetDelegates($owner)
	{
	$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)
	$service.Url = $ewsuri
	$service.GetDelegates($owner,$true)
	}

function GetFMA($identity)
	{
	Get-MailboxPermission $identity | Where-Object {$_.IsInherited -eq $false}
	}

function GetSendAs($identity)
	{
	Get-ADPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.ExtendedRights -eq 'Send-As'}
	}

function GetFolderPermission($mailbox,$folder)
	{
	Get-MailboxFolderPermission "$mailbox`:\$folder"
	}
#EndRegion

#Region Body
LoadAPI
try
	{
	$user = Get-User $mailboxOwner -Filter {RecipientType -eq 'UserMailbox'} -ErrorAction Stop
	}
catch
	{
	Write-Output "`"$mailboxOwner`" cannot be found or does not have a mailbox.  Verify the entered information."
	exit
	}

#Get list of delegates and permissions from EWS
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Delegates" -PercentComplete 0
$delegates = GetDelegates $user.WindowsEmailAddress.ToString()

#Get list of users with full mailbox access
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving FMA List" -PercentComplete 40
$fullMailboxAccess = GetFMA $user.Identity
$fmaSID = GetSID $fullMailboxAccess #Convert username to SID
if ($includeSendAs)
	{
	#Get list of users with send as permission from AD
	Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Send As List" -CurrentOperation "(This part takes the longest.)" -PercentComplete 70
	$sendAs = GetSendAs $user.Identity
	$saSID = GetSID $sendAs #Convert username to SID
	}
#Get permissions for additional folders
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving additional folder permissions" -PercentComplete 90
$deletedItemsPerm = GetFolderPermission $user.Identity 'Deleted Items'
$sentItemsPerm = GetFolderPermission $user.Identity 'Sent Items'

Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Completed $true

Write-Output "`n$($user.FirstName) $($user.LastName) has $($delegates.DelegateUserResponses.Count) delegate(s)"
#Loop through list of delegates
if ($delegates.DelegateUserResponses.Count -gt 0)
	{
	Write-Output "`nGlobal Delegate Meeting Request Handling: $($delegates.MeetingRequestsDeliveryScope)"
	$delegates.DelegateUserResponses | ForEach {
		Write-Output `n
		#Delegate account deleted in AD but still listed in list
		if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
			{
			Write-Output "*Orphaned Delegate"
			Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
			Write-Output "  property 0x684A101E to determine orphan entry."
			}
		else
			{
			$delegateDisplayName = $_.delegateuser.userid.displayname
			Write-output "*$delegateDisplayName `($($_.delegateuser.userid.primarysmtpaddress)`)"
			if ($fmaSID -match $_.delegateuser.UserId.SID)
				{
				Write-Output "  Mailbox-level permission: Full Mailbox Access"
				}
			else
				{
				Write-Output "  Mailbox-level permission: None"
				}
			if ($includeSendAs)
				{
				if ($saSID -match $_.delegateuser.UserId.SID)
					{
					Write-Output "  Send As permission: True"
					}
				else
					{
					Write-Output "  Send As permission: False"
					}
				}
			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"
			[array]$delegateDIPerm = $deletedItemsPerm | where {$_.User -eq $delegateDisplayName}
			if ($delegateDIPerm.Count -eq 1)
				{
				Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Deleted Items: None"
				}
			[array]$delegateSIPerm = $sentItemsPerm | where {$_.User -eq $delegateDisplayName}
			if ($delegateSIPerm.Count -eq 1)
				{
				Write-Output "    Sent Items: $($delegateSIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Sent Items: None"
				}

			Write-Output "  Receives Meeting Requests: $($_.delegateuser.receivecopiesofmeetingmessages)"
			Write-Output "  Can View Private Items: $($_.delegateuser.viewprivateitems)"
			}
		}
	}
Write-Output `n
#EndRegion

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.

TechEd 2011: WP7 Mango update a snoozer

By , May 18, 2011 7:43 AM

Microsoft announced more details of the Windows Phone 7 update (Mango), due out later this year.  They hyped the enterprise features, which are nothing to be proud of, IMO.  Support for searching you server-side mailbox.  Already in WM 6.x?  Check.  Support for IRM.  Already in WM 6.x?  Check.  Lync client. Communicator Mobile for WM 6.x to connect to OCS already?  Check.  A mobile client for Lync should have been released with Lync server RTM.  Conversation views.  Already in WM 6.x?  Check.

Where is the at-rest device encryption?  It is already a cliche to say that it is ironic when Microsoft’s own mobile OS doesn’t support all of their Exchange ActiveSync policies, one of them being the very thing whose absence will keep the OS out of the enterprise.  Because my company deals with PHI/PII, we require at-rest encryption, which means the only devices that we allow via EAS are iDevices, Android with Touchdown installed, and Windows Mobile 6.x.

One may argue that WP7 is a consumer device.  But MS has abandoned everything for WM 6.x.  They may say that WP7 is a consumer device, but they treat as their one-and-only OS for home and business.  They didn’t even release a TechEd app for WM for 6.x, only WP7 (and Android).  So if you release an app for 10,000 enterprise geeks at your premier technical conference for IT Pros and Developers on only your consumer-targeted mobile OS, what message are you really sending about who you target audience is?

The correct way to restrict users to specific devices with Exchange ActiveSync

By , March 1, 2011 9:58 AM

One way to control access to a mailbox with Exchange ActiveSync is to restrict a user to a specific device ID (or more than one), as described in this TechNet help page. The prerequisite listed on the page is only that the user be enabled for EAS. You will find, however, that if you list one or more device IDs in the ActiveSyncAllowedDevicesIDs property and attempt to sync with a device that is not in the list, it will still be able to.

The reason for this is not explained clearly in Microsoft documentation, but can be inferred by this good post on the Exchange blog. It details the flow logic when a device is accessing a mailbox and the order in which permissive and preventative access is handled. This image from the post sums up the flow:

When a device attempts to sync with a mailbox, the Allow/Block list on the user object is checked first, followed by any device access rules defined at the org level, and then the global access rule (the decision point labeled as Anything Unknown).  The default configuration at the org level is a permissive organization, where any device not explicitly blocked at the user level or does not match any device access rule is allowed.

The important distinction, however, is that specifying an allowed device ID at the user level is not exclusive, while specifying a blocked device ID is [effectively] exclusive.  This means that the access logic doesn’t stop at the user level when an allowed device ID is specified, though it does when a blocked device ID is specified.  Therefore, in a default permissive organization, specifying an allowed device ID for a user does not restrict the user to only that device.  In order to restrict a user to the device ID(s) specified on the user object, it is also necessary to define a device access rule and/or change the global access policy so that a device will otherwise be blocked (or quarantined).

To understand the action taken by Exchange when a device attempts to synchronize, I have made my own flowchart (click to zoom):

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()

Panorama Theme by Themocracy