Database seeding completion estimate script updated

Articles in the "Database seeding estimation" series

  1. Estimate the time to complete seeding a mailbox database copy
  2. Database seeding completion estimate script updated [This article]

The script has been updated to include the average throughput from the sampling duration in the output. This will help gauge why the seeding completion time may vary between multiple executions of the script, such as during vs. outside of business hours. Additionally, some variable names have been updated (to better reflect their contents), and per scripting best practices, the help has an added example so that all parameters are used at least once among all the examples. The inline code has been updated, as has the download.

  Get-DatabaseSeedingCompletion.zip (1.5 KiB)

Estimate the time to complete seeding a mailbox database copy

Articles in the "Database seeding estimation" series

  1. Estimate the time to complete seeding a mailbox database copy [This article]
  2. Database seeding completion estimate script updated

Seeding a mailbox database copy, especially across a WAN, can take days to complete. The shell doesn’t give any indication of the time it will complete, though it does show a progress bar and the amount of data written. If you want to know when the seeding will actually finish, you can extrapolate this from the throughput of the seeding, dividing that into the amount of data remaining to be copied.

The throughput of a copy being seeded is available as a performance counter, as are the percentage complete and the amount written. Using this information I wrote a script to estimate the date and time the database seeding will complete. Because throughput can vary greatly from second to second, the default sample duration is 60 seconds. You can specify any number of seconds, choosing accuracy or impatience. Retrieving performance counters can take some time (not even counting any sampling you may do), so a progress indicator will indicate any time that is occurring. (Three separate queries for performance counters occur to provide the output.)

To run the script, provide a database copy and, optionally, a sample duration. The copy will be validated that it is being seeded, then the seeding throughput will be retrieved for 60 seconds or the duration you specified. Then the amount of data written so far is retrieved, comparing that to the total size of the database, so that the remaining amount can be determined. That amount is divided by the average throughput during the sample duration and calculating the time it will be done. Bear in mind that this calculation does not include the time to seed the content index or copy and replay transaction logs that have been generated since seeding began. The output will show the percentage of the database size that has been seeded thus far and the estimated date and time the seeding will complete.

You can copy the code below or download the script via the link.

  Get-DatabaseSeedingCompletion.zip (1.5 KiB)

<#
	.Synopsis
		Estimate time of completion for database seeding
	.Description
		Using a customizable sample duration of the seeding throughput for a database copy,
		the script estimates the date and time the seeding will complete.  This does
		not include the time to seed the content index or copy transaction logs that have
		been accruing since the seeding started.
	.Parameter Identity
		The identity of a database copy, using the format Database\Server.
	.Parameter SampleDuration
		The number of seconds (in one-second intervals) to monitor the seeding throughput
		in order to calculate the completion time.  The default is 60 seconds.
	.Example
		Get-DatabaseSeedingCompletion.ps1 -Identity database1\server2
	.Example
		Get-DatabaseSeedingCompletion.ps1 -Identity database2\serverA -SampleDuration 360
	.Notes
		Version: 1.1
		Date: 11/22/13
#>
param
	(
	[parameter(Mandatory=$true,Position=0)][string]$Identity,
	[parameter(Mandatory=$false,Position=1)][int]$SampleDuration = 60
	)
$database = $Identity.Substring(0,$Identity.IndexOf('\'))
$server = $Identity.Substring($Identity.IndexOf('\') + 1)
$showProgress = $true
while ($showProgress -eq $true)
	{
	Write-Progress -Activity 'Getting seeding progress percentage...' -Status 'Retrieving'
	$percentCompleteCounter = Get-Counter -ComputerName $server -Counter "\MSExchange Replica Seeder($database)\Database Seeding Progress %" -ErrorAction 'SilentlyContinue'
	$showProgress = $false
	Write-Progress -Activity 'Getting seeding progress percentage...' -Status 'Retrieving' -Completed
	}
if (-not($percentCompleteCounter))
	{
	Write-Warning -Message "$Identity is not currently being seeded."
	}
else
	{
	$percentComplete = $percentCompleteCounter.CounterSamples[0].CookedValue
	
	#Get seeding throughput
	$showProgress = $true
	while ($showProgress -eq $true)
		{
		Write-Progress -Activity 'Getting seeding throughput...' -Status "Sampling for $SampleDuration seconds"
		$kBytesWrittenPerSecondCounter = Get-Counter -ComputerName $server -Counter "MSExchange Replica Seeder($database)\Database Seeding Bytes Written (KB/sec)" -MaxSamples $SampleDuration
		$showProgress = $false
		Write-Progress -Activity 'Getting seeding throughput...' -Status 'Sampling' -Completed
		}
	$kBytesWrittenPerSecondAverage = $kBytesWrittenPerSecondCounter | ForEach-Object {$_.CounterSamples[0].CookedValue} | Measure-Object -Average | Select-Object -ExpandProperty Average
	if ($kBytesWrittenPerSecondAverage -eq 0)
		{
		Write-Warning -Message 'Cannot estimate completion time because throughput for the sampling period is 0 KB.'
		}
	else
		{
		#Get bytes written so far
		$showProgress = $true
		while ($showProgress -eq $true)
			{
			Write-Progress -Activity 'Getting seeding amount written so far...' -Status 'Retrieving'
			$kBytesWrittenCounter = Get-Counter -ComputerName $server -Counter "MSExchange Replica Seeder($database)\Database Seeding Bytes Written (KB)"
			$showProgress = $false
			Write-Progress -Activity 'Getting seeding amount written so far...' -Status 'Retrieving' -Completed
			}
		$kBytesWrittenValue = $kBytesWrittenCounter.CounterSamples[0].CookedValue
		
		#Get DB size to calculate remaining amount
		$dbSizeRaw = (Get-MailboxDatabase -Identity $database -Status).DatabaseSize
		if ($dbSizeRaw.GetType().Name -eq 'ByteQuantifiedSize') #Object type when using EMS
			{
			$dbSize = $dbSizeRaw.ToBytes()
			}
		else
			{ #Object type when using remoting is string
			$dbSize = [int64]($dbSizeRaw.Split("(")[1].Split()[0])
			}
		$KBRemaining = $dbSize/1KB - $kBytesWrittenValue
		$estimatedMinutesToCompletion = $KBRemaining/$kBytesWrittenPerSecondAverage/60
		$completionTime = (Get-Date).AddMinutes($estimatedMinutesToCompletion)
		$outputObject = "" | Select-Object -Property Identity,PercentComplete,'Throughput(MB/sec)',EstimatedCompletion
		$outputObject.Identity = $Identity
		$outputObject.PercentComplete = $percentComplete
		$outputObject.'Throughput(MB/sec)' = [math]::Round($kBytesWrittenPerSecondAverage/1024,1)
		$outputObject.EstimatedCompletion = "{0:g}" -f $completionTime
		$outputObject
		}
	}

Delegate management module updated to v1.3.5

Articles in the "Mailbox Delegate Management" series

  1. Super duper delegate retrieval script
  2. PowerShell module for managing Exchange 2010 mailbox delegates
  3. Small update to delegate management module
  4. Delegate management module updated to v1.3.5 [This article]

The module has been updated so that the function that retrieves Send As permission will work in the Exchange Management Shell. I use implicit remoting rather than EMS, so I didn’t realize that the filter for Send-As needs to be different (though I don’t know why, which is aggravating). I previously also changed the default access method to use FMA instead of impersonation. If you want to use impersonation, uncomment line 96. The inline code and the download have been updated.

  DelegateManagement.zip (5.8 KiB)

Delete all empty subfolders of Deleted Items

At my company we use a retention policy to delete items in the Deleted Items folder that are older than 14 days. (This is implemented via Managed Folders, a.k.a. MRM 1.0.) Managed Folders, and even Retention Policies (MRM 2.0), act on items, not folders. A common scenario, though, is that users will soft-delete a folder as an easy way to move all of its items to the Deleted Items folder. After 14 days, the managed folder policy will delete the items in that folder, but it leaves the folder itself. Over time, users may have dozens of empty folders in the Deleted Items folder, and the only way they will ever be deleted is if users delete them one by one. It goes without saying (though I am obviously saying it anyway) that users don’t do this, so lots and lots of empty folders build up over time.

I don’t like this, so I wrote a script that deletes the empty subfolders of Deleted Items for all the mailboxes in the organization. (I actually wrote two scripts, one for the Deleted Items, and one for any specified parent folder.) The script uses the EWS Managed API. You can have any version installed, and it will check for one, starting with the most recent version. The intent of the script is to process all mailboxes in the org, so it retrieves all mailboxes and pipes them into a ForEach-Object loop:

(Get-Mailbox -ResultSize unlimited) | ForEach-Object {
	$EMSDeletedItemsFolderID = Get-FolderId -Mailbox $_.Identity
	$EWSDeletedItemsFolderID = Convert-EMSFolderID -EmailAddress $_.PrimarySMTPAddress -EMSFolderId $EMSDeletedItemsFolderID
	Remove-EmptySubfolder -EmailAddress $_.PrimarySMTPAddress -EWSFolderId $EWSDeletedItemsFolderID
	}

You can always change this line to restrict the returned mailboxes, or use the other script in the download, which supports pipelined input. Note the use of the parentheses when getting the collection of mailboxes. This is to avoid the pipeline constraint when using implicit remoting. The first function called is to get the folder ID of the Deleted Items folder for a given mailbox:

function Get-FolderId ($Mailbox)
	{
	$MailboxFolder = Get-MailboxFolderStatistics -Identity $Mailbox -FolderScope 'DeletedItems' | Select-Object -First 1
	$MailboxFolder.FolderId
	}

The FolderScope parameter is set to the Deleted Items folder, which is piped to Select-Object to limit the result to the top-level folder, ignoring any subfolders that will also be returned. The FolderId property is the function’s output.

The next step is to convert the folder ID from the management shell to one that is usable by Exchange Web Services:

function Convert-EMSFolderID ($EmailAddress, $EMSFolderId)
	{
	$EMSFolderId = $EMSFolderId.Replace('+','%2b')
	$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$ExchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
	$ExchangeService.AutodiscoverUrl($EmailAddress,{$true})    
	#Uncomment to use impersonation instead of FMA
	#$ExchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)   
	$aiItem = New-Object -TypeName Microsoft.Exchange.WebServices.Data.AlternateId        
	$aiItem.Mailbox = $EmailAddress
	$aiItem.UniqueId = $EMSFolderId     
	$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId        
	$convertedId = $ExchangeService.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)   
	$convertedId.UniqueId
	}

Any plus (+) signs are transposed to hex since they get in the way. A typical connection to EWS is then established. The AlternateId class is used to specify an ID and what format it is in. Folder IDs in the management shell’s cmdlets are in the OwaId format. To convert them to the EwsId format needed by EWS when referencing an item, the ConvertId() method of the ExchangeService class (the base class of the EWS connection) is used. The UniqueId property is the function’s output.

The last function called is to actually delete any subfolders of the folder with the ID the previous function returned:

function Remove-EmptySubfolder ($EmailAddress, $EWSFolderId)
	{
	$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$ExchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
	$ExchangeService.AutodiscoverUrl($EmailAddress,{$true})    
	#Uncomment to use impersonation instead of FMA
	#$ExchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)   
	#Bind to folder with specific ID
	$FolderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId($EWSFolderId)   
	$TargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ExchangeService,$FolderId)
	#Create view large enough to hold all of the search results to avoid paging
	$FolderView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderView(850)
	#Search filter for folders with no contents
	$SearchFilter = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::TotalCount,0)
	$SearchResults = $ExchangeService.FindFolders($TargetFolder.Id,$SearchFilter,$FolderView)
	if ($SearchResults.TotalCount -gt 0)
		{ 
    	foreach ($folder in $SearchResults.Folders)
			{ 
			#Exclude indirect subfolders
			if ($folder.ParentFolderId.UniqueId -eq $EWSFolderId)
				{
				Write-Output -InputObject "$EmailAddress`: $($folder.DisplayName)"
				$folder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
				}
			}
		}
	else
		{
    	Write-Output -InputObject "$EmailAddress`: No folders to delete"
		}
	}

You can see this function redundantly establishes a new EWS connection. This is because I was adapting functions used elsewhere for this script and I didn’t optimize it. The function may take less time to complete by using a session that has already been established, but I didn’t determine that or change the previous function to create the session variable in the parent scope so it could be reused. After the session initialization, the function connects to the mailbox and binds to the Deleted Items folder using the folder ID from the previous function.

In order to get the list of empty subfolders, a search is performed for folders that contain no items. This is done by creating a view that will hold the results and defining a search filter whose only restriction is that it contains no items. The search is executed and then looped through each returned folder. Because the search is recursive, the loop will ignore any folder whose parent folder is not Deleted Items. The folder is then deleted.

Write-Output is used, not only to save puppies, but so that you can see what is happening while still allowing you to save the output to file. This can be accomplished with Tee-Object, e.g.:

.\Remove-EmptyDeletedItemsSubfolder.ps1 | Tee-Object c:\deletedfolders.txt

One caveat of the script as written is that a subfolder that also contains subfolders, which may contain items, will still be deleted. Since I am deleting subfolders of Deleted Items, whose items are deleted after 14 days anyway, I am not too concerned about inadvertently deleting items. If this will be a problem for you, you will need to modify the script accordingly. (I may do so in the future.) Another workaround if this is a problem, is to use the second script (explained next) which has a parameter for skipping folders that have subfolders, regardless of their content.

While writing the script, I thought it might be beneficial to be able to run this process against any parent folder. So I converted the Deleted Items script into one that does just that. You can run this script with parameters that will do the exact same thing as the first script, but it also lets you specify any of the other folder scopes or a custom folder path. You can also indicate that you don’t want subfolders that have subfolders to be included (to avoid the potential issue using the first script). Lastly, this script requires that you provide a mailbox to act on. This can be manually specified with the Identity parameter, or you can pipe any number of mailboxes into it.

The FolderScope and FolderPath parameters are mutually exclusive. Use FolderScope when you want to easily specify one of the inbuilt scopes available to the Get-MailboxFolderStatistics cmdlet. This could be, for example, Inbox, RssSubscriptions, or SentItems. Use FolderPath when you want to specify a literal path to the parent folder, such as /Inbox/Subfolder1. The path to the folder is validated to ensure it starts with a forward slash and does not end with one, which matches the output of the Get-MailboxFolderStatistics cmdlet.

The functions in this script are broken down to be inline (not using functions) in order to support pipelining. The other change is supporting the exclusion of subfolders that have subfolders. This is done by modifying the search to use an additional restriction:

$SearchFilter1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::TotalCount,0)
	$SearchFilter = $SearchFilter1
	if ($ExcludeFoldersWithSubfolders)
		{
		$SearchFilter2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::ChildFolderCount,0)
		$SearchFilterCollection = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And)
		$SearchFilterCollection.Add($SearchFilter1)
		$SearchFilterCollection.Add($SearchFilter2)
		$SearchFilter = $SearchFilterCollection
		}
	$SearchResults = $ExchangeService.FindFolders($TargetFolder.Id,$SearchFilter,$FolderView)

If the parameter to exclude subfolders is used, a search filter collection has to be used because of the need for multiple criteria. You define the search filters the same (using a unique name for each variable), but then you create a search filter collection object, add the search filters to the collection, and then use that object as the search filter in the search execution.

Both scripts are included in the download file, or you can copy the code below.

  Remove-EmptySubfolders.zip (3.4 KiB)

Remove-EmptyDeletedItemsSubfolder

<#
	.Synopsis
		Deletes empty subfolders of Deleted Items folder.
	.Description
		Gets all mailboxes in the organization and searches each mailbox's
		Deleted Items folder for immediate subfolders that do not contain
		any items.
	.Notes
		Version: 1.0
		Date: 8/2/13
#>

#Paths to EWS Managed API DLL
$ewsAPIPaths = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll",
	"C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll",
	"C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll",
	"C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

#Test if any version of API is installed
foreach ($path in $ewsAPIPaths)
	{
	if (Test-Path -Path $path)
		{
		Add-Type -Path $path
		$apiFound = $true
		break
		}
	}

if (-not($apiFound))
	{
	Write-Error -Message 'The Exchange Web Services Managed API is required to run this script.' -Category NotInstalled
	break
	}

#Convert folder ID from EMS to folder ID used by EWS
function Convert-EMSFolderID ($EmailAddress, $EMSFolderId)
	{
	$EMSFolderId = $EMSFolderId.Replace('+','%2b')
	$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$ExchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
	$ExchangeService.AutodiscoverUrl($EmailAddress,{$true})    
	#Uncomment to use impersonation instead of FMA
	#$ExchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)   
	$aiItem = New-Object -TypeName Microsoft.Exchange.WebServices.Data.AlternateId        
	$aiItem.Mailbox = $EmailAddress
	$aiItem.UniqueId = $EMSFolderId     
	$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId        
	$convertedId = $ExchangeService.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)   
	$convertedId.UniqueId
	}

function Get-FolderId ($Mailbox)
	{
	$MailboxFolder = Get-MailboxFolderStatistics -Identity $Mailbox -FolderScope 'DeletedItems' | Select-Object -First 1
	$MailboxFolder.FolderId
	}

function Remove-EmptySubfolder ($EmailAddress, $EWSFolderId)
	{
	$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$ExchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
	$ExchangeService.AutodiscoverUrl($EmailAddress,{$true})    
	#Uncomment to use impersonation instead of FMA
	#$ExchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)   
	#Bind to folder with specific ID
	$FolderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId($EWSFolderId)   
	$TargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ExchangeService,$FolderId)
	#Create view large enough to hold all of the search results to avoid paging
	$FolderView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderView(850)
	#Search filter for folders with no contents
	$SearchFilter = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::TotalCount,0)
	$SearchResults = $ExchangeService.FindFolders($TargetFolder.Id,$SearchFilter,$FolderView)
	if ($SearchResults.TotalCount -gt 0)
		{ 
    	foreach ($folder in $SearchResults.Folders)
			{ 
			#Exclude indirect subfolders
			if ($folder.ParentFolderId.UniqueId -eq $EWSFolderId)
				{
				Write-Output -InputObject "$EmailAddress`: $($folder.DisplayName)"
				$folder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
				}
			}
		}
	else
		{
    	Write-Output -InputObject "$EmailAddress`: No folders to delete"
		}
	}

(Get-Mailbox -ResultSize unlimited) | ForEach-Object {
	$EMSDeletedItemsFolderID = Get-FolderId -Mailbox $_.Identity
	$EWSDeletedItemsFolderID = Convert-EMSFolderID -EmailAddress $_.PrimarySMTPAddress -EMSFolderId $EMSDeletedItemsFolderID
	Remove-EmptySubfolder -EmailAddress $_.PrimarySMTPAddress -EWSFolderId $EWSDeletedItemsFolderID
	}

Remove-EmptySubfolder

<#
	.Synopsis
		Deletes subfolders that do not contain any items.
	.Description
		Given a mailbox folder, immediate subfolders that do not contain any items will be deleted.
	.Parameter Identity
		Mailbox identity to act on.
	.Parameter FolderScope
		Valid FolderScope value from Get-MailboxFolderStatistics cmdlet, excluding All.
		This is the parent folder to use for empty subfolders.
	.Parameter FolderPath
		The path to the folder, including the foward slash prefix.  This is the parent
		folder to use for empty subfolders.
	.Parameter ExcludeFoldersWithSubFolders
		Switch to indicate that any subfolder of the parent folder that itself has subfolders
		should not be included for deletion.  This is because the subfolder may contain
		items that would also be deleted.
	.Example
		.\Remove-EmptySubfolder.ps1 JohnDoe -FolderScope DeletedItems
	.Example
		(Get-Mailbox) | .\Remove-EmptySubfolder.ps1 -FolderPath "/Inbox/Folder1"
	.Notes
		Version: 1.0
		Date: 8/30/13
#>
param
	(
	[Parameter(ValueFromPipeline=$true,Position=0)][string]$Identity,
	[Parameter(Mandatory=$true,ParameterSetName='folderscope')]
	[ValidateSet('Calendar','Contacts','ConversationHistory','DeletedItems','Drafts',
	'Inbox','JunkEmail','Journal','ManagedCustomFolder','Notes','Outbox','Personal',
	'RecoverableItems','RssSubscriptions','SentItems','SyncIssues','Tasks')][string]$FolderScope,
	[Parameter(Mandatory=$true,ParameterSetName='folderpath')]
	[ValidatePattern('(?# Path must begin with / and not end with /)^/.*[^/]$')][string]$FolderPath,
	[switch]$ExcludeFoldersWithSubfolders
	)

begin {
	#Paths to EWS Managed API DLL
	$ewsAPIPaths = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll",
		"C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll",
		"C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll",
		"C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

	#Test if any version of API is installed
	foreach ($path in $ewsAPIPaths)
		{
		if (Test-Path -Path $path)
			{
			Add-Type -Path $path
			$apiFound = $true
			break
			}
		}

	if (-not($apiFound))
		{
		Write-Error -Message 'The Exchange Web Services Managed API is required to run this script.' -Category NotInstalled
		break
		}
	}
	
process	{
	$EmailAddress = (Get-Mailbox -Identity $Identity).PrimarySMTPAddress
	
	#Get EMS folder ID
	if ($FolderScope)
		{
		$EMSFolder = Get-MailboxFolderStatistics -Identity $Identity -FolderScope $FolderScope | Select-Object -First 1
		$EMSFolderId = $EMSFolder.FolderId
		}
	else
		{
		$EMSFolder = Get-MailboxFolderStatistics -Identity $Identity | Where-Object {$_.FolderPath -eq $FolderPath}
		if ($EMSFolder)
			{
			$EMSFolderId = $EMSFolder.FolderId
			}
		else
			{
			Write-Output -InputObject "$EmailAddress`: No matching parent folder"
			break
			}
		}

	#Convert folder ID from EMS to folder ID used by EWS
	$EMSFolderId = $EMSFolderId.Replace('+','%2b')
	$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
	$ExchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)    
	$ExchangeService.AutodiscoverUrl($EmailAddress,{$true})    
	#Uncomment to use impersonation instead of FMA
	#$ExchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress)   
	$aiItem = New-Object -TypeName Microsoft.Exchange.WebServices.Data.AlternateId        
	$aiItem.Mailbox = $EmailAddress
	$aiItem.UniqueId = $EMSFolderId     
	$aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId        
	$convertedId = $ExchangeService.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId)   
	$EWSFolderId = $convertedId.UniqueId
	
	#Bind to folder with specific ID
	$FolderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId($EWSFolderId)   
	$TargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ExchangeService,$FolderId)
	#Create view large enough to hold all of the search results to avoid paging
	$FolderView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderView(850)
	#Search filter for folders with no contents
	$SearchFilter1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::TotalCount,0)
	$SearchFilter = $SearchFilter1
	if ($ExcludeFoldersWithSubfolders)
		{
		$SearchFilter2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::ChildFolderCount,0)
		$SearchFilterCollection = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And)
		$SearchFilterCollection.Add($SearchFilter1)
		$SearchFilterCollection.Add($SearchFilter2)
		$SearchFilter = $SearchFilterCollection
		}
	$SearchResults = $ExchangeService.FindFolders($TargetFolder.Id,$SearchFilter,$FolderView)
	if ($SearchResults.TotalCount -gt 0)
		{ 
    	foreach ($folder in $SearchResults.Folders)
			{ 
			#Exclude indirect subfolders
			if ($folder.ParentFolderId.UniqueId -eq $EWSFolderId)
				{
				Write-Output -InputObject "$EmailAddress`: $($folder.DisplayName)"
				$folder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
				}
			}
		}
	else
		{
    	Write-Output -InputObject "$EmailAddress`: No subfolders to delete"
		}
	}
end	{}

Small update to delegate management module

Articles in the "Mailbox Delegate Management" series

  1. Super duper delegate retrieval script
  2. PowerShell module for managing Exchange 2010 mailbox delegates
  3. Small update to delegate management module [This article]
  4. Delegate management module updated to v1.3.5

The Exchange 2010 delegate management module has been updated to v1.3.2. It includes a couple of small changes to resolve an issue if you use the module inside the Exchange Management Shell (as opposed to running vanilla PowerShell with implicit remoting). The inline code displayed in the main post has been updated, as has the downloadable copy.

  DelegateManagement.zip (5.8 KiB)

PowerShell module for managing Exchange 2010 mailbox delegates

Articles in the "Mailbox Delegate Management" series

  1. Super duper delegate retrieval script
  2. PowerShell module for managing Exchange 2010 mailbox delegates [This article]
  3. Small update to delegate management module
  4. Delegate management module updated to v1.3.5

I have converted my script that displays a mailbox’s delegates into a module that adds the ability to add, modify, and remove a mailbox’s delegates. The benefit of the module is that it negates the need to create an Outlook profile for the owner/manager in order to manipulate his or her delegates. The module requires any version of the EWS Managed API and the Exchange cmdlets via remoting, as detailed below.

Upon importing the module, it will check for any version of the EWS Managed API, starting with version 2.0 and working its way back. This allows for the freedom to have any version installed instead of requiring everyone who uses it to ensure they have the specific one I used when writing it. After the API is loaded, it will check for the Exchange cmdlets. If not loaded into the shell, it will retrieve a list of Exchange servers from Active Directory and attempt to connect to one until successful. If neither the API nor remoting to Exchange is successful, the module will fail to load, telling you why. (The module doesn’t distinguish between the cmdlets being available because they were locally loaded from the snap-in or from remoting. However, since certain cmdlets will fail when not executed remotely because they bypass RBAC, you need to be sure that you have not locally loaded the snap-in.)

Access to a mailbox is done using full mailbox access. If you want to use impersonation, you will want to uncomment line 96. Granting impersonation rights is explained here. The URL used for EWS is determined by autodiscover of the owner/manager mailbox.

These are the cmdlets the module exposes:

Get-MailboxDelegate

The alias for Get-MailboxDelegate is gmd.  It is basically the same as my Get-Delegates.ps1 script, but it has gotten a makeover to support pipelining into and out of. The -Identity parameter (aliased to -Owner and -Manager) is any standard Exchange identity (display name, email address, alias, dn, etc.) and it supports pipelining by property name. If the objects you are pipelining into Get-MailboxDelegate don’t have a property name of Identity, then you will need to use a ForEach loop and use $_.PropertyName to designate which property should be used.

Without any other parameters, all delegates will be retrieved. If you want to get only a specific delegate, you can use the -Delegate parameter. The default output will be to list the properties, but since it is now a collection of objects, you can choose to output it to a table, to a grid view, or export to a file, using the appropriate cmdlets. You can also use these output cmdlets to select only the properties you want. For example, if you only care about the private items property you could use ft owner,delegate,viewprivate. Or, if you only want those who actually can view private items, you could run something like this:

(Get-DistributionGroupMember <DLIdentity>) | gmd | where {$_.ViewPrivate -eq $true} | ft owner,delegate

Note that I encapsulated in parentheses the command that I pipeline into Get-MailboxDelegate. This is necessary to avoid the concurrent pipeline limitation in remote PowerShell. It is only necessary if the command prior to the pipeline will be running a cmdlet that leverages remoting. Another option is to store the results of the prior command in a variable and then pipeline that into Get-MailboxDelegate.

All of the module’s cmdlets have built-in help, so you can use PowerShell’s help cmdlet to learn the details of all of them, such as the parameters and their descriptions, usage examples, etc.

Add-MailboxDelegate

The alias for Add-MailboxDelegate is amd. To use this cmdlet, provide an owner and a delegate. You can optionally specify folder permission for Inbox, Calendar, Tasks, Contacts, Sent Items, and Deleted Items; if private items are viewable; if meeting requests are to be received; and the owner’s global handling of meeting requests. I didn’t include the option of setting permission for Journal or Notes because, well, who uses them? The ability to set the permission for Sent Items and Deleted Items is to accommodate those who use GPO to have Outlook store messages sent from another user in that person’s Sent Items folder, and likewise for messages deleted from another mailbox. The option to set the meeting request delivery scope applies to the owner, not the delegate being added, so it is only necessary to include it if you are adding a delegate and you want to change the current setting.

Set-MailboxDelegate

The alias for Set-MailboxDelegate is smd. Use this cmdlet to change any settings for an existing delegate (or to change the meeting request delivery scope for the owner). Provide the owner and the existing delegate to modify and, optionally, which setting you want to change. All other settings will remain as is. If you want to change just the meeting request delivery scope for the owner, specify any existing delegate, but not any other settings (except the delivery scope). Unlike the valid roles for folder permission with Add-MailboxDelegate, you can use None if you want to remove a folder permission. If you want to remove the ability to view private items or to not receive meeting requests, use -ViewPrivateItems:$false or -ReceiveMeetingRequests:$false, respectively. The colon is necessary because both parameters are switches, so their inclusion alone in the command means true, whereas to explicitly set them to false means using the syntax above. (The cmdlet checks if either switch is actually present in the command, so don’t be concerned that not including a switch implies that it should be false.)

Remove-MailboxDelegate

The alias for Remove-MailboxDelegate is rmd. Provide the owner and the delegate to remove. That’s it.

All of the cmdlets perform the expected error checking: the owner is a valid mailbox; any delegate to add, modify, or remove is a valid mailbox; adding a delegate when the delegate already exists; modifying or removing a delegate that is not an existing delegate; using a valid (albeit the limited subset exposed in the API) role for a folder permission; and using a valid meeting request delivery scope.

Download the module or view/copy the code below:

  DelegateManagement.zip (5.8 KiB)

#Exchange 2010 mailbox delegate management module
#v1.3.5 9/9/13
#1.3.5 Changed Send-As function to work in EMS
#1.3.4 Changed default access to use FMA instead of impersonation

#Paths to EWS Managed API DLL
$ewsAPIPaths = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.1\Microsoft.Exchange.WebServices.dll",
"C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

#Test if any version of API is installed before loading module functions
foreach ($path in $ewsAPIPaths)
	{
	if (Test-Path $path)
		{
		Add-Type -Path $path
		$apiFound = $true
		break
		}
	}

if (-not($apiFound))
	{
	Write-Error "The Exchange Web Services Managed API is required to use this module." -Category NotInstalled
	break
	}

function Get-ExchangeServerName
	{
	$configNC = ([ADSI]"LDAP://RootDse").configurationNamingContext
	$search = New-Object DirectoryServices.DirectorySearcher([ADSI]"LDAP://$configNC")
	$search.Filter = "(&(objectClass=msExchExchangeServer)(versionNumber>=1937801568))"
	$search.PageSize=1000
	$search.PropertiesToLoad.Clear()
	[void] $search.PropertiesToLoad.Add("networkaddress")
	$search.FindAll()
	}

function Get-ServerFqdnFromNetworkAddress($server)
	{
   	$server.properties["networkaddress"] |
      Where-Object {$_.ToString().StartsWith("ncacn_ip_tcp")} | ForEach-Object {$_.ToString().SubString(13)}
	}

function Connect-ExchangeServer($server)
	{
	try
		{
		Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$server/powershell" -Name 'RemoteExchange') -AllowClobber -DisableNameChecking -ErrorAction Stop | Out-Null
		$true
		}
	catch
		{
		$false
		}
	}
	
function Connect-RemoteExchangeServer
	{
	$exchangeServers = Get-ExchangeServerName
	$serverFQDN = $exchangeServers | ForEach-Object {Get-ServerFqdnFromNetworkAddress $_}
	#Loop through servers until connection is successful
	for ($i = 0; $i -lt $serverFQDN.Length; $i++)
		{
		$attemptResult = Connect-ExchangeServer $serverFQDN[$i]
		if ($attemptResult)
			{break}
		}
	if ($attemptResult)
		{$true}
	else
		{$false}
	}

#Test for Exchange cmdlets before loading any module functions
$testcmd = Get-Command Get-Mailbox -ErrorAction SilentlyContinue
if (-not($testcmd))
	{
	$connectResult = Connect-RemoteExchangeServer
	if (-not($connectResult))
		{
		Write-Error "Unable to connect to any Exchange server." -Category ConnectionError
		break
		}
	}

#Region Helper Functions
function Connect-WebServices ($smtpAddress)
	{
	$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2 
	$global:exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) 
 	#Use autodiscover instead of hard-coded URL
	$exchangeService.AutodiscoverUrl($smtpAddress) 
	#Impersonate mailbox
	#$exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress)
	New-Object Microsoft.Exchange.WebServices.Data.Mailbox($smtpAddress)
	}
 
function Find-Mailbox ($identity)
	{
	try 
		{
		Get-Mailbox $identity -ErrorAction Stop
		}
	catch
		{
		Write-Progress 'Done' -Completed -Status " "
		Write-Error "A mailbox cannot be found that matches the input string $identity." -ErrorAction Stop -Category ObjectNotFound
		}
	}

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

function Get-FMA($identity)
	{
	Get-MailboxPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.User -notlike 'S-1-5-21*'}
	}
	
function Get-SendAs($identity)
	{
	Get-ADPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.ExtendedRights -like '*Send-As*'}
	}

function Get-FolderPermission($mailbox,$folder)
	{
	Get-MailboxFolderPermission "$mailbox`:\$folder"
	}
	
function Set-FolderPermission($owner,$delegate,$folder,$role)
	{
	$folderPerm = Get-FolderPermission $owner.Identity $folder
	[array]$delegateFolderPerm = $folderPerm | Where-Object {$_.User -eq $delegate.DisplayName}
	#Run appropriate cmdlet based on delegate presence in ACL
	if ($delegateFolderPerm.Count -eq 1)
		{
		try
			{
			Set-MailboxFolderPermission -Identity "$($owner.Identity):\$folder" -User $delegate.Identity -AccessRights $role -ErrorAction Stop | Out-Null
			}
		catch
			{
			$false
			}
		}
	else
		{
		try
			{
			Add-MailboxFolderPermission -Identity "$($owner.Identity):\$folder" -User $delegate.Identity -AccessRights $role -ErrorAction Stop | Out-Null
			}
		catch
			{
			$false
			}
		}
	}

function Convert-StringPermissionToEnum($role)
	{
	switch ($role)
		{
		'Reviewer' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Reviewer}
		'Author' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Author}
		'Editor' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::Editor}
		'None' {[Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None}
		}
	}

function Convert-StringDeliveryScopeToEnum($scope)
	{
	switch ($scope)
		{
		'DelegatesOnly' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesOnly}
		'DelegatesAndOwner' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndMe}
		'DelegatesAndInfoToOwner' {[Microsoft.Exchange.WebServices.Data.MeetingRequestsDeliveryScope]::DelegatesAndSendInformationToMe}
		}
	}
#EndRegion

#Region Main Functions
function Add-MailboxDelegate
	{
	<#
	.Synopsis
		Add a mailbox as a delegate of an owner's mailbox.
	.Description
		Add a mailbox delegate, optionally specifying permission to various folders,
		whether private items are viewable by the delegate, and if the delegate should receive
		meeting requests for the owner.
	.Parameter Owner
		Identity string of the user whose mailbox is to have the delegate.
	.Parameter Delegate
		Identity string of the user who is to be added to owner's mailbox.
	.Parameter InboxPermission
		Role to assign to the Inbox folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter CalendarPermission
		Role to assign to the Calendar folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter TasksPermission
		Role to assign to the Tasks folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter ContactsPermission
		Role to assign to the Contacts folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter SentItemsPermission
		Role to assign to the Sent Items folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter DeletedItemsPermission
		Role to assign to the Deleted Items folder.  Valid roles are Reviewer, Author, and Editor.
	.Parameter ViewPrivateItems
		Enable the delegate to view items marked as private.
	.Parameter ReceiveMeetingRequests
		Enable the delegate to receive meeting requests for the owner.
	.Parameter MeetingRequestDeliveryScope
		Specify how meeting requests should be handled for the owner.  Valid scopes are DelegatesOnly,
		DelegatesAndOwner, and DelegatesAndInfoToOwner.  Note that this parameter applies to all delegates.
	.Example
		Add-MailboxDelegate Username DelegateUsername -InboxPermission Editor -CalendarPermission Editor -ViewPrivateItems
	.Example
		Add-MailboxDelegate -Owner domain\username -Delegate  -CalendarPermission Editor -ReceiveMeetingRequests
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate,
		[ValidateSet('Reviewer','Author','Editor')][string]$InboxPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$CalendarPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$TasksPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$ContactsPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$SentItemsPermission,
		[ValidateSet('Reviewer','Author','Editor')][string]$DeletedItemsPermission,
		[Alias("PI")][switch]$ViewPrivateItems,
		[Alias("MR")][switch]$ReceiveMeetingRequests,
		[ValidateSet('DelegatesOnly','DelegatesAndOwner','DelegatesAndInfoToOwner')][string]$MeetingRequestDeliveryScope
		)

	#Validate mailboxes
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate
	
	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName

	#Get EWS mailbox reference
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, without folder permissions
	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Retrieving existing delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$false)
	
	#Check if user is already a delegate
	if ($currentDelegates.DelegateUserResponses.Count -gt 0)
		{
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				Write-Progress -Activity "Adding Mailbox Delegate" -Completed -Status " "
				Write-Host "$delegateFirstName $delegateLastName is already a delegate of $ownerFirstName $ownerLastName. Use Set-MailboxDelegate to update an existing delegate."
				return
				}
			}
		}

	Write-Progress -Activity "Adding Mailbox Delegate" -Status "Adding delegate" -PercentComplete 75
	#Create EWS delegate object
	$delegateUser = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser($mbDelegate.PrimarySMTPAddress.ToString())
	
	#Set private items
	if ($ViewPrivateItems)
		{
		$delegateUser.ViewPrivateItems = $true
		}
	
	#Set meeting request receipt
	if ($ReceiveMeetingRequests)
		{
		$delegateUser.ReceiveCopiesOfMeetingMessages = $true
		}
	
	#Set permissions on folders
	if ($InboxPermission)
		{
		$delegateUser.Permissions.InboxFolderPermissionLevel = Convert-StringPermissionToEnum $InboxPermission
		}
	if ($CalendarPermission)
		{
		$delegateUser.Permissions.CalendarFolderPermissionLevel = Convert-StringPermissionToEnum $CalendarPermission
		}
	if ($TasksPermission)
		{
		$delegateUser.Permissions.TasksFolderPermissionLevel = Convert-StringPermissionToEnum $TasksPermission
		}
	if ($ContactsPermission)
		{
		$delegateUser.Permissions.ContactsFolderPermissionLevel = Convert-StringPermissionToEnum $ContactsPermission
		}
	if ($SentItemsPermission)
		{
		$SIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Sent Items' $SentItemsPermission
		}
	if ($DeletedItemsPermission)
		{
		$DIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Deleted Items' $DeletedItemsPermission
		}
		
	#Build delegate collection object to use in EWS method
	$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1 
	$delegateArray[0] = $delegateUser

	#Set new meeting request delivery scope if specified
	if ($MeetingRequestDeliveryScope)
		{
		$addResponse = $exchangeService.AddDelegates($EWSMailbox, (Convert-StringDeliveryScopeToEnum $MeetingRequestDeliveryScope), $delegateArray)
		}
	else
		{
		$addResponse = $exchangeService.AddDelegates($EWSMailbox, $null, $delegateArray)
		}
		
	Write-Progress -Activity "Adding Mailbox Delegate" -Completed -Status " "
	if ($addResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
		{
		Write-Host "$delegateFirstName $delegateLastName has been added as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
		if ($SIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Sent Items folder." -ForegroundColor Yellow
			}
		if ($DIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Deleted Items folder." -ForegroundColor Yellow
			}
		}
	else
		{
		Write-Host "An error occurred adding $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
		}
	}

function Set-MailboxDelegate
	{
	<#
	.Synopsis
		Update the settings for an existing delegate of an owner's mailbox.
	.Description
		Update an existing mailbox delegate, specifying any changes to folder permissions,
		whether private items are viewable by the delegate, or if the delegate should receive
		meeting requests for the owner.
	.Parameter Owner
		Identity string of the user whose mailbox has the delegate.
	.Parameter Delegate
		Identity string of the user whose delegate settings are to be updated.
	.Parameter InboxPermission
		Role to assign to the Inbox folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter CalendarPermission
		Role to assign to the Calendar folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter TasksPermission
		Role to assign to the Tasks folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter ContactsPermission
		Role to assign to the Contacts folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter SentItemsPermission
		Role to assign to the Sent Items folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter DeletedItemsPermission
		Role to assign to the Deleted Items folder.  Valid roles are Reviewer, Author, Editor, and None.
	.Parameter ViewPrivateItems
		Enable the delegate to view items marked as private.
	.Parameter ReceiveMeetingRequests
		Enable the delegate to receive meeting requests for the owner.
	.Parameter MeetingRequestDeliveryScope
		Specify how meeting requests should be handled for the owner.  Valid scopes are DelegatesOnly,
		DelegatesAndOwner, and DelegatesAndInfoToOwner.  Note that this parameter applies to all delegates.
	.Example
		Set-MailboxDelegate Username DelegateUsername -InboxPermission Editor -CalendarPermission Editor -ViewPrivateItems
	.Example
		Set-MailboxDelegate -Owner domain\username -Delegate  -CalendarPermission Editor -ReceiveMeetingRequests
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$InboxPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$CalendarPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$TasksPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$ContactsPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$SentItemsPermission,
		[ValidateSet('Reviewer','Author','Editor','None')][string]$DeletedItemsPermission,
		[Alias("PI")][switch]$ViewPrivateItems,
		[Alias("MR")][switch]$ReceiveMeetingRequests,
		[ValidateSet('DelegatesOnly','DelegatesAndOwner','DelegatesAndInfoToOwner')][string]$MeetingRequestDeliveryScope
		)

	#Validate mailboxes
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate
	
	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName
	
	#Get EWS mailbox reference
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, with folder permissions
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Retrieving existing delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true)
	
	#Confirm user is already a delegate
	$delegateMatch = $false
	if ($currentDelegates.DelegateUserResponses.Count -eq 0)
		{
		Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
		Write-Host "$ownerFirstName $ownerLastName does not have any delegates. `
				Use Add-MailboxDelegate to add a new delegate."
				return		
		}
	elseif ($currentDelegates.DelegateUserResponses.Count -gt 0)
		{
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				#Modify existing delegate object instead of creating new one so existing settings
				#can be preserved
				$delegateUser = $currentDelegate.DelegateUser
				$delegateMatch = $true
				}
			}
		if (-not($delegateMatch))
			{
			Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
			Write-Host "$delegateFirstName $delegateLastName is not a delegate of $ownerFirstName $ownerLastName. Use Add-MailboxDelegate to add a new delegate."
			return
			}
		}
	
	Write-Progress -Activity "Updating Mailbox Delegate" -Status "Updating delegate" -PercentComplete 75
	
	#Set private items if included
	if ($MyInvocation.BoundParameters.ContainsKey('ViewPrivateItems'))
		{
		$delegateUser.ViewPrivateItems = $ViewPrivateItems
		}
	
	#Set meeting request receipt if included
	if ($MyInvocation.BoundParameters.ContainsKey('ReceiveMeetingRequests'))
		{
		$delegateUser.ReceiveCopiesOfMeetingMessages = $ReceiveMeetingRequests
		}
	
	#Set permissions on folders
	if ($InboxPermission)
		{
		$delegateUser.Permissions.InboxFolderPermissionLevel = Convert-StringPermissionToEnum $InboxPermission
		}
	if ($CalendarPermission)
		{
		$delegateUser.Permissions.CalendarFolderPermissionLevel = Convert-StringPermissionToEnum $CalendarPermission
		}
	if ($TasksPermission)
		{
		$delegateUser.Permissions.TasksFolderPermissionLevel = Convert-StringPermissionToEnum $TasksPermission
		}
	if ($ContactsPermission)
		{
		$delegateUser.Permissions.ContactsFolderPermissionLevel = Convert-StringPermissionToEnum $ContactsPermission
		}
	if ($SentItemsPermission)
		{
		$SIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Sent Items' $SentItemsPermission
		}
	if ($DeletedItemsPermission)
		{
		$DIPermResponse = Set-FolderPermission $mbOwner $mbDelegate 'Deleted Items' $DeletedItemsPermission
		}
		
	#Build delegate collection object to use in EWS method
	$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1 
	$delegateArray[0] = $delegateUser

	#Set new meeting request delivery scope if specified
	if ($MeetingRequestDeliveryScope)
		{
		$updateResponse = $exchangeService.UpdateDelegates($EWSMailbox, (Convert-StringDeliveryScopeToEnum $MeetingRequestDeliveryScope), $delegateArray)
		}
	else
		{
		$updateResponse = $exchangeService.UpdateDelegates($EWSMailbox, $null, $delegateArray)
		}
		
	Write-Progress -Activity "Updating Mailbox Delegate" -Completed -Status " "
	if ($updateResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
		{
		Write-Host "$delegateFirstName $delegateLastName has been updated as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
		if ($SIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Sent Items folder." -ForegroundColor Yellow
			}
		if ($DIPermResponse -eq $false)
			{
			Write-Host "An error occurred adding the delegate permission to the Deleted Items folder." -ForegroundColor Yellow
			}
		}
	else
		{
		Write-Host "An error occurred updating $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
		}
	}
	
function Remove-MailboxDelegate
	{
	<#
	.Synopsis
		Remove a delegate from an owner's mailbox.
	.Description
		Remove a supplied mailbox delegate from a supplied owner's mailbox.
	.Parameter Owner
		Identity string of the user whose mailbox has the delegate.
	.Parameter Delegate
		Identity string of the user who is to be removed from the owner's mailbox.
	.Example
		Remove-MailboxDelegate user@domain.com delegate@domain.com
	.Example
		Remove-MailboxDelegate -Owner domain\username -Delegate 
	.Notes
		Version: 1.01
		Date: 3/5/13
	#>
	[CmdletBinding()]
	param (
		[Parameter(Position=0,Mandatory=$true)][Alias("Manager")][string]$Owner,
		[Parameter(Position=1,Mandatory=$true)][string]$Delegate
		)

	#Validate mailboxes
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Validating owner and delegate mailboxes" -PercentComplete 0
	$mbOwner = Find-Mailbox $Owner
	$mbDelegate = Find-Mailbox $Delegate

	$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
	$ownerLastName = (Get-User $mbOwner.Identity).LastName
	$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
	$delegateLastName = (Get-User $mbDelegate.Identity).LastName
	
	#Get EWS mailbox reference
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 25
	$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()
	
	#Get collection of delegates, without folder permissions
	Write-Progress -Activity "Removing Mailbox Delegate" -Status "Retrieving delegates" -PercentComplete 50
	$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$false)
	if ($currentDelegates.DelegateUserResponses.Count -eq 0)
		{
		Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
		Write-Host $ownerFirstName $ownerLastName "does not have any delegates."
		}
	else
		{
		$delegateToRemove = @()
		$delegateMatch = $false
		Write-Progress -Activity "Removing Mailbox Delegate" -Status "Removing delegate" -PercentComplete 75
		foreach ($currentDelegate in $currentDelegates.DelegateUserResponses)
			{
			if ($currentDelegate.DelegateUser.UserId.PrimarySMTPAddress -eq $mbDelegate.PrimarySMTPAddress.ToString())
				{
				#Add userid object to collection of delegates to remove
				$delegateToRemove += $currentDelegate.DelegateUser.UserId
				$delegateMatch = $true
				}
			}	
		if (-not($delegateMatch))
			{
			Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
			Write-Host $mbDelegate.PrimarySMTPAddress "is not a delegate of" $mbOwner.PrimarySMTPAddress "."
			}
		else
			{
			#Remove delegate from owner's mailbox
			$removeResponse = $exchangeService.RemoveDelegates($EWSMailbox, $delegateToRemove)
			Write-Progress -Activity "Removing Mailbox Delegate" -Completed -Status " "
			if ($removeResponse[0].Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success)
				{
				Write-Host "$delegateFirstName $delegateLastName has been removed as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Green
				}
			else
				{
				Write-Host "An error occurred removing $delegateFirstName $delegateLastName as a delegate of $ownerFirstName $ownerLastName." -ForegroundColor Red
				}
			}
		}
	}

function Get-MailboxDelegate
	{
	<#
	.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 Identity
		Identity string of the user whose mailbox has the delegates.  Owner and Manager are valid
		aliases for this parameter.
	.Parameter Delegate
		Identity string of the delegate you want to retrieve.  If omitted, all delegates
		are retrieved.
	.Parameter IncludeSendAs
		Switch to indicate that you want Send As permission to be included.
	.Example
		Get-MailboxDelegate user@domain.com -includesendas
	.Example
		Get-MailboxDelegate domain\username
	.Notes
		Version: 1.7
		Date: 3/12/13
	#>
	param (
		[Parameter(Position=0,Mandatory=$true,HelpMessage="Identity of mailbox owner",ValueFromPipelineByPropertyName=$true)]
		[Alias("Owner")][Alias("Manager")][string]$Identity,
		[Parameter(Position=1)][string]$Delegate,
		[Alias("SA")][switch]$includeSendAs #Perform Send As lookup (takes longer)	
		)
	
	process
		{
		#Validate owner mailbox
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Validating owner mailbox" -PercentComplete 0
		$mbOwner = Find-Mailbox $Identity
		
		#Validate delegate mailbox
		if ($Delegate)
			{
			Write-Progress -Activity "Getting Mailbox Delegate" -Status "Validating delegate mailbox" -PercentComplete 5
			$mbDelegate = Find-Mailbox $Delegate
			$delegateFirstName = (Get-User $mbDelegate.Identity).FirstName
			$delegateLastName = (Get-User $mbDelegate.Identity).LastName
			}
		
		$ownerFirstName = (Get-User $mbOwner.Identity).FirstName
		$ownerLastName = (Get-User $mbOwner.Identity).LastName

		#Get EWS mailbox reference
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Connecting to EWS" -PercentComplete 10
		$EWSMailbox = Connect-WebServices $mbOwner.PrimarySMTPAddress.ToString()

		#Get list of delegates and permissions from EWS
		Write-Progress -Activity "Getting Mailbox Delegate" -Status "Retrieving delegates" -PercentComplete 20
		#Get collection of delegates, with folder permissions
		if ($mbDelegate)
			{
			#Retrieve only the specified delegate
			$delegateUser = New-Object Microsoft.Exchange.WebServices.Data.UserId($mbDelegate.PrimarySMTPAddress.ToString())
			$delegateArray = New-Object Microsoft.Exchange.WebServices.Data.UserId[] 1 
			$delegateArray[0] = $delegateUser
			$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true,$delegateArray)
			}
		else
			{
			$currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true)
			}

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

		#Build output
		$outputArr = New-Object System.Collections.ArrayList
		#Loop through list of delegates
		if ($currentDelegates.DelegateUserResponses.Count -gt 0)
			{
			if ($mbDelegate -and $currentDelegates.DelegateUserResponses[0].ErrorCode -eq 'ErrorNotDelegate')
				{
				Write-Host "$delegateFirstName $delegateLastName is not a delegate of $ownerFirstName $ownerLastName."
				}
			else
				{
				$currentDelegates.DelegateUserResponses | ForEach-Object {
					#Create custom object with property names
					$outputItem = "" | Select-Object Owner,Delegate,MeetingHandling,FMA,SendAs,Calendar,Inbox,Contacts,Tasks,DeletedItems,SentItems,ReceiveMeetings,ViewPrivate,Error,ErrorNote
					$outputItem.Owner = $mbOwner.DisplayName
					$outputItem.MeetingHandling = $currentDelegates.MeetingRequestsDeliveryScope
					if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
						{#Delegate account deleted in AD but still listed in list
						$outputItem.Error = "Orphan"
						$outputItem.ErrorNote = "Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml property 0x684A101E to determine orphan entry."
						}
					elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
						{
						$outputItem.Error = "Misconfigured"
						$outputItem.ErrorNote = "Missing from Freebusy Data folder or publicDelegates attribute."
						}
					elseif ($_.Result -eq 'Error')
						{
						$outputItem.Error = "UnknownError"
						$outputItem.ErrorNote = $_.ErrorMessage
						}
					else
						{
						$delegateDisplayName = $_.delegateuser.userid.displayname
						$outputItem.Delegate = $delegateDisplayName
						if ($fmaSID -match $_.delegateuser.UserId.SID)
							{
							$outputItem.FMA = $true
							}
						else
							{
							$outputItem.FMA = $false
							}
						if ($includeSendAs)
							{
							if ($saSID -match $_.delegateuser.UserId.SID)
								{
								$outputItem.SendAs = $true
								}
							else
								{
								$outputItem.SendAs = $false
								}
							}
						$outputItem.Calendar = $_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString()
						$outputItem.Inbox = $_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString()
						$outputItem.Contacts = $_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString()
						$outputItem.Tasks = $_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString()
						
						#Construct Deleted Items permission output
						[array]$delegateDIPerm = $deletedItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
						if ($delegateDIPerm.Count -eq 1)
							{
							$delegateDIPermValue = $delegateDIPerm[0].AccessRights[0].ToString()
							}
						else
							{
							$delegateDIPermValue = 'None'
							}
						$outputItem.DeletedItems = $delegateDIPermValue
						
						#Construct Sent Items permission output
						[array]$delegateSIPerm = $sentItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
						if ($delegateSIPerm.Count -eq 1)
							{
							$delegateSIPermValue = $delegateSIPerm[0].AccessRights[0].ToString()
							}
						else
							{
							$delegateSIPermValue = 'None'
							}
						$outputItem.SentItems = $delegateSIPermValue
						
						$outputItem.ReceiveMeetings = $_.delegateuser.receivecopiesofmeetingmessages
						$outputItem.ViewPrivate = $_.delegateuser.viewprivateitems
						
						$outputArr.Add($outputItem) | Out-Null
						}
					}
				}
			}
		else
			{
			Write-Host "$ownerFirstName $ownerLastName has no delegates."
			}
		$outputArr
		}
	}
#EndRegion

Set-Alias amd Add-MailboxDelegate
Set-Alias smd Set-MailboxDelegate
Set-Alias rmd Remove-MailboxDelegate
Set-Alias gmd Get-MailboxDelegate
Export-ModuleMember -Function *-MailboxDelegate
Export-ModuleMember -Alias *