Delegate management module updated to support Exchange Online

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
  5. Delegate management module updated to support Exchange Online [This article]

The module for managing Exchange mailbox delegates has been updated with support for Exchange Online. In its current version (v1.4) you can use one mode or the other. The default mode is on-premises, but you can change this on demand to use Exchange Online by using Set-DelegateMananagementMode, a new cmdlet added in this version. If you change it on demand, you will be prompted for your Office 365 credentials. If you will be exclusively working with Exchange Online, you can change the line near the top of the module to default to using that method. In that case, you will be prompted for credentials the first time you run a cmdlet.

It is my intention to update the module to support a hybrid environment, but I first need to set up one in my lab in order to test it.

  DelegateManagement.zip (5.8 KiB)

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

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

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)