Category: Exchange 2010

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

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>

Exchange 2010 prerequisite installation script updated

By , June 28, 2010 9:49 AM

Edit on August 24, 2010: Script updated to version 1.5. See notes here.

I have made a couple of minor updates to the prerequisite installation script. One is changing the way the source directory is determined. Instead of hardcoding the directory, the directory that the script is running from will be used. This allows you to simply put all files in the same directory as the script and run it from anywhere (except for UNC).

Another change is a correction when checking for installation binaries. I failed to account for the backslash between the directory and the filename so, even when the file exists, the script wouldn’t see it.

The last change is moving the NETTCPPortSharing service setting after the Windows components installation completes so that the setting is immediately configured. This allows you to begin the Exchange installation without rebooting.

The download and inline code from the original post have been updated.

  Prepare-Exchange2010Install.zip (4.7 KiB)

Script to install all Exchange 2010 prerequisites

By , June 10, 2010 1:55 PM

Edit on August 24, 2010: Script updated to version 1.5. See notes here.

There are already several sites which have scripts to install Exchange 2010 prerequisites, but they all seem to fall short in one aspect or another. Some make you choose the right script if you are on 2008 or 2008 R2. Others download some files but not others. None make sure that WinRM is installed, which is needed for PowerShell v2. So I started with a great script and made it better. My starting point is Bhargav Shukla’s script, which in turn started elsewhere.

My script checks whether the server is running Windows Server 2008 SP2 or Windows Server 2008 R2. (If you aren’t running SP2, it will tell you.) Because the command to add the Windows components is different between them (ServerManagerCmd.exe in DOS and Add-WindowsFeature in PowerShell), the appropriate command will be run.

Instead of just downloading the 2007 Office Filter Pack, I added the option to download each file (including WinRM) if it isn’t installed and not in the installation source directory. I also chose to install the 2010 Office Filter Pack, which includes support for 2007.

If Windows Remote Management isn’t installed, it will install it and reboot the server. You don’t have to uninstall PowerShell v1 to do this, so you can run the script and install WinRM, then run the script again after the reboot to continue (which is then implicitly using PowerShell v2).

I also added functions to enable remote PowerShell and enable the Windows Firewall service. The latter is required for Exchange setup to be successful so it can create the firewall exceptions. You can always disable the service after setup is complete.

I added the option to install the HT and CAS roles together, should that apply to you.

If you want to download the required files ahead of time and put them in the same directory as the script, they are listed below (direct download links). Note that I renamed the filter pack installation file to 2010FilterPack64bit.exe in order to distinguish it from the Office 2007 Filter Pack installation file. If you have the 2007 Filter pack installed, the 2010 install will replace it upon installation. If you manually download the filter pack, though, remember to rename it.

You can copy the script inline below, or download it via the link at the bottom of the post.

#Installs prerequisites necessary to install Exchange 2010 on
#Windows 2008 SP2 or Windows 2008 R2.
#Version 1.3
#Last modified: June 28, 2010

#Set installation source to same directory as script execution
$sourcePath = Split-Path -Parent $MyInvocation.MyCommand.Path

Write-Host 'Using ' -NoNewline
Write-Host $sourcePath -ForegroundColor DarkGreen -NoNewline
Write-Host ' as the installation source.'

# Detect correct OS here and exit if no match
if ((Get-WMIObject win32_OperatingSystem).Version -eq '6.1.7600')
	{$os = 'R2'}
elseif ((Get-WMIObject win32_OperatingSystem).Version -eq '6.0.6002')
	{$os = 'R1'}
else
	{
	Write-Host 'This script requires Windows Server 2008 with SP2, or R2, which this is not.' -ForegroundColor Red -BackgroundColor Black
	break
	}

#Installation files and properties (filename, shortname, displayname, download URL, size)
$fileWinRM = ('Windows6.0-KB968930-x64.msu','WinRM','Windows Remote Management Framework','http://download.microsoft.com/download/2/8/6/28686477-3242-4E96-9009-30B16BED89AF/Windows6.0-KB968930-x64.msu','14MB')
$fileNET35 = ('dotnetfx35.exe','.NET 3.5','.NET 3.5 SP1','http://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe','235MB')
$fileNET35HF = ('NDP35SP1-KB958484-x64.exe','.NET 3.5 hotfix','.NET 3.5 hotfix','http://download.microsoft.com/download/B/4/2/B42197BD-AEE1-4FE6-8CB3-29D60D0C3727/Windows6.0-KB958483-x64.msu','1.4MB')
$fileOFP = ('2010FilterPack64bit.exe','Office 2010 Filter Pack','Office 2010/2007 Filter Pack','http://download.microsoft.com/download/0/A/2/0A28BBFA-CBFA-4C03-A739-30CCA5E21659/FilterPack64bit.exe','4MB')

Function InstallApp($app)
	{
	switch ($app)
		{
		'WinRM'
			{
			$appArray = $fileWinRM
			$kb = 'KB968930'
			}
		'NET35'
			{
			$appArray = $fileNET35
			$checkExpression = "test-path 'HKLM:Software\Microsoft\NET Framework Setup\NDP\v3.5'"
			}
		'NET35HF'
			{
			$appArray = $fileNET35HF
			$checkExpression = "test-path 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Updates\Microsoft .NET Framework 3.5 SP1\SP1\KB958484'"
			}
		'OFP'
			{
			$appArray = $fileOFP
			$checkExpression = "test-path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\{95140000-2000-0409-1000-0000000FF1CE}'"
			}
		}
	trap
		{
		Write-Host ''
		Write-Host "There was a problem downloading or installing $($appArray[1])." -ForegroundColor Red
		Write-Host ''
		break
		}
	#Check for existing installation
	Write-Host "Verifying $($appArray[2]) is installed..." -NoNewline
	if ($app -eq 'WinRM')
		{
		$hfInst = Get-WMIObject Win32_QuickFixEngineering | where {$_.HotFixID -eq $kb}
		if ($hfInst)
			{
			$bInstalled = $true
			}
		else
			{
			$bInstalled = $false
			}
		}
	else
		{
		if (Invoke-Expression $checkExpression)
			{
			$bInstalled = $true
			}
		else
			{
			$bInstalled = $false
			}
		}
	if ($bInstalled)
		{
		Write-Host "$($appArray[2]) is installed." -ForegroundColor Green
		return
		}
	Write-Host "$($appArray[2]) is not installed." -ForegroundColor Red
	Write-Host "Installing $($appArray[2])..." -NoNewline

	#Install app:  Check for existing installation file.
	$fullPath = $sourcePath+"\$($appArray[0])"
	if (!(Test-Path $fullPath))
		{
		Write-Host ''
		Write-Host "$($appArray[0]) not found in source path." -ForegroundColor Yellow
		$dl = Read-Host "Do you want to download it now? ($($appArray[4]))(Y/N)"
		if ($dl -ne 'y')
			{
			Write-Host "You have chosen to not download the $($appArray[1]) installation file."
			Write-Host "Put $($appArray[0]) in the source directory and run the script again."
			break
			}
		else
			{
			Write-Host "Downloading $($appArray[1])..." -NoNewline
			$dlClient = New-Object System.Net.WebClient
			$dlClient.DownloadFile($appArray[3],$fullPath)
			if (!(Test-Path $fullPath))
				{
				Write-Host ''
				Write-Host "There was a problem downloading $($appArray[1])." -ForegroundColor Red
				Write-Host ''
				}
			else
				{
				Write-Host 'done.' -ForegroundColor Green
				}
			}
		}

	#Install app: Run installation.
	if ($app -eq 'WinRM')
		{
		$expression = "wusa $fullPath /quiet"
		Invoke-Expression $expression
		Write-Host 'External update process started...Be patient, it takes time.' -ForegroundColor Yellow
		Write-Host ''
		Write-Host 'When the WinRM installation is complete, the system will automatically reboot.'
		Write-Host 'Then you can rerun the script to continue.  This script will now end.'
		break
		}
	else
		{
		if ($app -eq 'NET35HF')
			{$arguments = '/passive /norestart'}
		else
			{$arguments = '/quiet /norestart'}
		$process = [System.Diagnostics.Process]::Start($fullPath,$arguments)
		$process.WaitForExit()
		Write-Host "$($appArray[1]) installation complete." -ForegroundColor Green
		}
	}	

Function InstallNET35()
	{
	InstallApp('NET35')
	InstallApp('NET35HF')
	}

Function SetTCPSharing()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem setting the NET TCP Port Sharing service to Automatic startup.' -ForegroundColor Red
		Write-Host 'The service must be set to Automatic for Exchange setup to be successful.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Set NETTCPPortSharing to Automatic
	Write-Host 'Configuring the NET TCP Port Sharing service...' -NoNewline
	Set-Service NetTcpPortSharing -StartupType Automatic
	Write-Host 'done.' -ForegroundColor Green
	}

Function EnableRemoting()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem configuring the system for remote PowerShell.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Enable Remote PowerShell for Exchange administration from workstations
	Write-Host 'Enabling system for remote PowerShell connections...'
	Enable-PSRemoting -force
	Write-Host 'Remote PowerShell configuration is done.' -ForegroundColor Green
	}

Function EnableFirewall()
	{
	trap
		{
		Write-Host ''
		Write-Host 'There was problem starting the Windows Firewall service.' -ForegroundColor Red
		Write-Host 'The firewall service must be running during Exchange setup.  It can be stopped after it completes.' -ForegroundColor Red
		Write-Host ''
		return
		}
	#Ensure Windows Firewall is running or Exchange install will fail
	Write-Host 'Starting the Windows Firewall service...' -NoNewline
	Set-Service 'MpsSvc' -StartupType Automatic -Status Running
	Write-Host 'done.' -ForegroundColor Green
	}

if ($os -eq 'R1')
	{
	$ht = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Hub.xml'
	$cas = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-CAS.xml'
	$mbx = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-MBX.xml'
	$um = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-UM.xml'
	$edge = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Edge.xml'
	$typical = '. ServerManagerCmd.exe -ip '+$sourcePath+'\Exchange-Typical.xml'
	}
elseif ($os -eq 'R2')
	{
	$ht = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server -restart'
	$cas = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Web-ISAPI-Ext,Web-Digest-Auth,Web-Dyn-Compression,NET-HTTP-Activation,RPC-Over-HTTP-Proxy -restart'
	$mbx = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server -restart'
	$um = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Desktop-Experience -restart'
	$edge = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,ADLDS -restart'
	$typical = 'Add-WindowsFeature NET-Framework,RSAT-ADDS,Web-Server,Web-Basic-Auth,Web-Windows-Auth,Web-Metabase,Web-Net-Ext,Web-Lgcy-Mgmt-Console,WAS-Process-Model,RSAT-Web-Server,Web-ISAPI-Ext,Web-Digest-Auth,Web-Dyn-Compression,NET-HTTP-Activation,RPC-Over-HTTP-Proxy -restart'
	Import-Module ServerManager
	}
$opt = 'None'

InstallApp('WinRM')

clear
if ($opt -ne 'None') {write-host 'Last command: '$opt -foregroundcolor Yellow}
write-host
write-host 'Exchange Server 2010 Prerequisites Installation'
write-host 'Please select which role you are going to install:'
write-host
write-host '1)  Hub Transport'
write-host '2)  Client Access Server'
write-host '3)  Mailbox'
write-host '4)  Unified Messaging'
write-host '5)  Edge Transport'
write-host '6)  Typical (CAS\HT\Mailbox)'
write-host '7)  Client Access and Hub Transport'
write-host
write-host '9)  Configure NetTCP Port Sharing service'
write-host '    Required for the Client Access Server role' -foregroundcolor yellow
write-host '    Automatically set for options 2,6, and 7' -foregroundcolor yellow
write-host '10) Install 2010 Office System Converter: Microsoft Filter Pack'
write-host '    Required if installing Hub Transport or Mailbox Server roles' -foregroundcolor yellow
write-host '    Automatically set for options 1,3,6, and 7' -foregroundcolor yellow
Write-Host '11) Enable PowerShell Remoting'
Write-Host '    Automatically set for options 1,2,3,4,6, and 7' -ForegroundColor Yellow
write-host
write-host '13) Restart the Server'
write-host '14) End'
write-host
Write-Host 'Note: Using ' -NoNewline
Write-Host $sourcePath -ForegroundColor DarkGreen -NoNewline
Write-Host ' as the installation source.'
$opt = Read-Host 'Select an option.. [1-14]? '

switch ($opt)
	{
	1{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $ht
	  	}
	2{
		InstallNET35; EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $cas
        SetTCPSharing
		}
	3{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $mbx
		}
	4{
		InstallNET35; EnableRemoting; EnableFirewall
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $um
		}
	5{
		InstallNET35; EnableFirewall
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $edge
		}
	6{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $typical
        SetTCPSharing
		}
	7{
		InstallNET35; InstallApp('OFP'); EnableFirewall; EnableRemoting
		Write-Host 'Beginning Windows components installation...'
		Invoke-Expression $cas
        SetTCPSharing
		}
	9 { SetTCPSharing }
	10 { InstallApp('OFP') }
	11 { EnableRemoting }
	13 { Restart-Computer }
	14 {write-host 'Exiting...'}
	default {write-host "You haven't selected any of the available options."}
	}

  Prepare-Exchange2010Install.zip (4.7 KiB)

Disable Exchange ActiveSync for users on Exchange 2010

By , May 12, 2010 11:09 AM

As with all previous versions of Exchange, the default permission in Exchange 2010 for user accounts is to allow Exchange ActiveSync. I don’t like this option and am surprised that Microsoft still doesn’t provide the ability to reverse this. Policy at my company requires you to sign a waiver acknowledging that we, as IT at the company, have the right to wipe your phone and any personal data that may be on it.

The AD attribute for controlling access to Exchange ActiveSync is still msExchOMAAdminWirelessEnable. This attribute has been around since Exchange 2003, controlling access to Outlook Mobile Access (OMA) and whether Automatic Up-to-Date Notifications (AUTD), the precursor to Direct Push, was enabled. Since then, OMA and AUTD have both been discontinued, and you can’t even disable Direct Push anymore in 2010. (I don’t know why you would do so anyway. I assume it was to control high data charges before everyone started using unlimited data plans.)

So the only value in the attribute that has any effect is 4, which disables Exchange ActiveSync. Any other value, including <not set>, has no impact, thereby allowing the user to sync. I have a reason for wanting to set the attribute directly in AD, but you can accomplish the same thing in the Exchange Management Shell by using Set-CASMailbox user -ActiveSyncEnabled:$false.

Panorama Theme by Themocracy