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	{}

Lync call handling script updated

Articles in the "Lync call handling script" series

  1. Trigger something whenever you’re on a Lync call
  2. Update to Lync call handling script
  3. Lync call handling script updated [This article]

The call handling script has been updated to version 1.4.4.  It resolves an issue that took me quite awhile to diagnose, where it would sometimes hang when signing out of Lync.  When the client state change event occurs, the corresponding function would check if you are signed in or not.  If not, it will unregister the off-hook handler event so it can be created anew when you sign in again.

The problem was because the client state change event fires multiple times when you sign out: once when signing out and again when you are signed out.  Since I was running the cmdlet to unregister the off-hook handler for any state that wasn’t equal to being signed in, the client state handler function was being called again (and attempting to unregister the off-hook handler event) before it could finish doing so the first time.  This caused the PowerShell session to hang.  The script now only unregisters the event if you are signed out, rather than on any event that isn’t equal to being signed in.

The script has also been updated to work with Lync 2013.  You will want to comment line 62 (and uncomment 63) if you are using Communicator or Lync 2010, or leave it as is if you are using Lync 2013.

  Monitor-LyncSelfActivityChange.zip (640.5 KiB)

My journey from Windows CE to Android

After over 14 years using some kind of Microsoft-powered mobile device, I recently got a Google Nexus 4.  I thought I would look back at the devices I have used over the years.  Most have long been recycled, but a few of them are in a drawer (or perhaps even the garage).

Google Nexus 4 (LG E960)
Released: November, 2012
Google Nexus 4
AT&T Tilt2 (HTC Rhodium)
Released: October, 2009
AT&T Tilt 2
Samsung BlackJack (Samsung SGH-i607)
Released: November, 2006
Samsung BlackJack
Cingular 2125 (HTC Faraday)
Released: March, 2006
Cingular 2125
Cingular 8125 (HTC Wizard)
Released: January, 2006
Cingular 8125
Sprint PPC-6700 (HTC Apache)
Released: October, 2005
Sprint-PPC6700
Siemens SX66 (HTC Blue Angel)
Released: December, 2004
Siemens SX66
Audiovox SMT-5600 (HTC Typhoon)
Releases: November, 2004
Audiovox SMT5600
Audiovox PPC-4100
Released: June, 2004
Audiovox PPC-4100
Hitachi SH-G1000
Released: July, 2003
Hitachi G1000
Siemens SX56 (HTC Wallaby)
Released: October, 2002
Siemens SX56
Casio Cassiopeia E-100
Released: May, 1999
Casio E100
Philips Nino 300
Released: September, 1998
Philips Nino