Last active
July 13, 2017 02:29
-
-
Save blakeja/0291e824ff92258aa62566109579a7d4 to your computer and use it in GitHub Desktop.
Remove duplicate items in Exchange public folders
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#region Synopsis | |
<# | |
.SYNOPSIS | |
Remove-DuplicatePFItems | |
.DESCRIPTION | |
This script will scan a public folder and remove duplicate items. | |
.NOTES | |
- Microsoft Exchange Web Services (EWS) Managed API 1.2 or up is required. | |
- Must be logged in as administrator with admin permissions to target folders | |
.PARAMETER EwsDllPath | |
Path to Exchange WebServices DLL, defaults to: | |
"C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll". | |
.PARAMETER EwsUrl | |
Exchange Web Services Url. | |
.PARAMETER ExchangeVersion | |
Release version of Exchange Server. | |
Options: Exchange2007_SP1 (Default), Exchange2010, Exchange2010_SP1, Exchange2010_SP2, Exchange2013 or Exchange2013_SP1. | |
.PARAMETER FolderPath | |
Full path to public folder. | |
.PARAMETER Recursive | |
Whether or not to recurse through folder structure, if set to $false, only items in the | |
specified folder path with be processed. | |
.PARAMETER WhatIf | |
If -WhatIf is set to $false, a dry run will be performed and no duplicate messages will be deleted. | |
.EXAMPLE | |
.\Remove-DuplicatePfItems.ps1 -FolderPath "Root052\oil.com" -WhatIf $true | |
#> | |
#endregion | |
#region Parameters | |
param ( | |
[Parameter(Mandatory = $false)] | |
[string]$EwsDllPath = "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll", | |
[Parameter(Mandatory = $true)] | |
[string]$EwsUrl, | |
[Parameter(Mandatory = $false)] | |
[ValidateSet("Exchange2007_SP1","Exchange2010","Exchange2010_SP1","Exchange2010_SP2","Exchange2013","Exchange2013_SP1")] | |
[string]$ExchangeVersion = "Exchange2007_SP1", | |
[Parameter(Mandatory = $true)] | |
[string]$FolderPath, | |
[Parameter(Mandatory = $false)] | |
[bool]$Recursive = $true, | |
[Parameter(Mandatory = $false)] | |
[bool]$WhatIf = $false | |
) | |
#endregion | |
#region Functions | |
Function Process-Items($folder) | |
{ | |
$temp = $null | |
$folderDetail = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$baseFolder.Id) | |
$itemIds = [activator]::createinstance(([type]'System.Collections.Generic.List`1').makegenerictype([Microsoft.Exchange.WebServices.Data.ItemId])) | |
$itemList = @{} | |
$offset = 0 | |
$totalItems = 0 | |
$itemCount = 0 | |
$note = 0 | |
$appt = 0 | |
$contact = 0 | |
$dl = 0 | |
$task = 0 | |
$post = 0 | |
$default = 0 | |
$notedupe = 0 | |
$apptdupe = 0 | |
$contactdupe = 0 | |
$dldupe = 0 | |
$taskdupe = 0 | |
$postdupe = 0 | |
$defaultdupe = 0 | |
$duplicateItemsRemovedInThisFolder = 0 | |
do | |
{ | |
$itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($maxItemBatchSize, $offset) | |
$itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow | |
$itemView.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties) | |
$sendCancellationsMode = $null | |
$affectedTaskOccurrences = [Microsoft.Exchange.WebServices.Data.AffectedTaskOccurrence]::AllOccurrences | |
$itemSearchResults = $exchangeService.FindItems($folder.Id, $itemView) | |
Write-Log "Processing $offset of $($folderDetail.TotalCount) items in $($folder.DisplayName)" | |
if ($itemSearchResults.Items.Count -gt 0) | |
{ | |
foreach($Item in $itemSearchResults.Items) | |
{ | |
$key = "" | |
switch ($Item.ItemClass) | |
{ | |
"IPM.Note" | |
{ | |
$Item = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($exchangeService,$Item.Id) | |
$note++ | |
$key = $Item.DateTimeReceived.ToString() | |
if ($Item.InternetMessageId) | |
{ | |
$key += "," + $Item.InternetMessageId | |
} | |
if ($Item.DateTimeSent) | |
{ | |
$key += "," + $Item.DateTimeSent.ToString() | |
} | |
if ($Item.Sender) | |
{ | |
$key += "," + $Item.Sender.Address | |
} | |
} | |
"IPM.Appointment" | |
{ | |
$appt++ | |
if ($Item.Subject) | |
{ | |
$key += $Item.Subject | |
} | |
if ($Item.Location) | |
{ | |
$key += "," + $Item.Location | |
} | |
if ($Item.Start) | |
{ | |
$key += "," + $Item.Start.ToString() | |
} | |
if ($Item.End) | |
{ | |
$key += "," + $Item.End.ToString() | |
} | |
} | |
"IPM.Contact" | |
{ | |
$contact++ | |
if ($Item.FileAs) | |
{ | |
$key += $Item.FileAs | |
} | |
if ($Item.GivenName) | |
{ | |
$key += "," + $Item.GivenName | |
} | |
if ($Item.Surname) | |
{ | |
$key += "," + $Item.Surname | |
} | |
if ($Item.CompanyName) | |
{ | |
$key += "," + $Item.CompanyName | |
} | |
if ($Item.PhoneNUmbers.TryGetValue("BusinessPhone", [ref]$temp)) | |
{ | |
$key += "," + $temp | |
} | |
if ($Item.PhoneNUmbers.TryGetValue("HomePhone", [ref]$temp)) | |
{ | |
$key += "," + $temp | |
} | |
if ($Item.PhoneNUmbers.TryGetValue("MobilePhone", [ref]$temp)) | |
{ | |
$key += "," + $temp | |
} | |
} | |
"IPM.DistList" | |
{ | |
$dl++ | |
if ($Item.FileAs) | |
{ | |
$key += $Item.FileAs | |
} | |
if ($Item.Members) | |
{ | |
$key += "," + $Item.Members.Count.ToString() | |
} | |
} | |
"IPM.Task" | |
{ | |
$task++ | |
if ($Item.Subject) | |
{ | |
$key += $Item.Subject | |
} | |
if ($Item.StartDate) | |
{ | |
$key += "," + $Item.StartDate.ToString() | |
} | |
if ($Item.DueDate) | |
{ | |
$key += "," + $Item.DueDate.ToString() | |
} | |
if ($Item.Status) | |
{ | |
$key += "," + $Item.Status | |
} | |
} | |
"IPM.Post" | |
{ | |
$post++ | |
if ($Item.Subject) | |
{ | |
$key += $Item.Subject | |
} | |
if ($Item.Size) | |
{ | |
$key += "," + $Item.Size.ToString() | |
} | |
} | |
Default | |
{ | |
$default++ | |
$key = $Item.DateTimeReceived.ToString() | |
if ($Item.Subject) | |
{ | |
$key += "," + $Item.Subject | |
} | |
} | |
} | |
$itemCount++ | |
if ($itemList.contains($key)) | |
{ | |
switch ($Item.ItemClass) | |
{ | |
"IPM.Note" | |
{ | |
$notedupe++ | |
} | |
"IPM.Appointment" | |
{ | |
$apptdupe++ | |
} | |
"IPM.Contact" | |
{ | |
$contactdupe++ | |
} | |
"IPM.DistList" | |
{ | |
$dldupe++ | |
} | |
"IPM.Task" | |
{ | |
$taskdupe++ | |
} | |
"IPM.Post" | |
{ | |
$postdupe++ | |
} | |
Default | |
{ | |
$defaultdupe++ | |
} | |
} | |
Write-Log "Duplicate will be deleted: $key" $false | |
$itemIds.Add($Item.Id) | |
$duplicateItemsRemovedInThisFolder++ | |
# When removing task or appointment, set SendCancellationsMode/AffectedTaskOccurrences | |
switch ($Item.ItemClass) | |
{ | |
"IPM.Appointment" | |
{ | |
if (!($sendCancellationsMode)) | |
{ | |
$sendCancellationsMode = [Microsoft.Exchange.WebServices.Data.SendCancellationsMode]::SendToNone | |
} | |
} | |
"IPM.Task" | |
{ | |
if ($Item.Recurrence -and $affectedTaskOccurrences -ne [Microsoft.Exchange.WebServices.Data.AffectedTaskOccurrence]::SpecifiedOccurrenceOnly) | |
{ | |
$affectedTaskOccurrences = [Microsoft.Exchange.WebServices.Data.AffectedTaskOccurrence]::SpecifiedOccurrenceOnly | |
} | |
} | |
Default | |
{ | |
} | |
} | |
} | |
else | |
{ | |
$itemList[$key] = $Item.Id.UniqueId.ToString() | |
} | |
} | |
} | |
else | |
{ | |
Write-Log "No items found in folder $($folder.DisplayName)" | |
} | |
$offset = $offset + $maxItemBatchSize | |
} while($itemSearchResults.MoreAvailable) | |
Write-Log "" | |
Write-Log "Summary for folder $($folder.DisplayName)" | |
Write-Log "" | |
Write-Log "notes: $note" | |
Write-Log "appt: $appt" | |
Write-Log "contact: $contact" | |
Write-Log "dl: $dl" | |
Write-Log "task: $task" | |
Write-Log "post: $post" | |
Write-Log "default: $default" | |
Write-Log "" | |
Write-Log "notes dupes: $notedupe" | |
Write-Log "appt dupes: $apptdupe" | |
Write-Log "contact dupes: $contactdupe" | |
Write-Log "dl dupes: $dldupe" | |
Write-Log "task dupes: $taskdupe" | |
Write-Log "post dupes: $postdupe" | |
Write-Log "default dupes: $defaultdupe" | |
Write-Log "" | |
Write-Log "message total: $itemCount" | |
Write-Log "dupe total: $duplicateItemsRemovedInThisFolder" | |
Write-Log "" | |
if ($itemIds.Count -gt 0) | |
{ | |
try | |
{ | |
if ($whatIf -eq $false) | |
{ | |
for ($index = 0; $index -lt $itemIds.Count; $index += $maxItemBatchSize) | |
{ | |
$upperIndex = $index + $maxItemBatchSize | |
if ($upperIndex -ge $itemIds.Count) | |
{ | |
$count = $itemIds.Count - $index | |
} | |
else | |
{ | |
$count = $maxItemBatchSize | |
} | |
$batch = $itemIds.GetRange($index, $count) | |
Write-Log "Removing $($batch.Count) duplicate items from folder $($folder.DisplayName)" | |
$result = $exchangeService.DeleteItems($batch, [Microsoft.Exchange.WebServices.Data.DeleteMode]::$deleteMode, $sendCancellationsMode, $affectedTaskOccurrences) | |
} | |
if (!($result)) | |
{ | |
throw "Error occurred whenremoving items, result was null or empty" | |
} | |
} | |
else | |
{ | |
Write-Log "-whatIf is set to $true, skipping removal of $($itemIds.Count) items from folder $($folder.DisplayName)" | |
} | |
} | |
catch | |
{ | |
throw "Error: Problem removing items: $($error[0])" | |
} | |
$itemIds = [activator]::createinstance(([type]'System.Collections.Generic.List`1').makegenerictype([Microsoft.Exchange.WebServices.Data.ItemId])) | |
} | |
else | |
{ | |
Write-Log "No duplicate items found in folder $($folder.DisplayName)" | |
} | |
} | |
Function Find-AllFolders($baseFolder) | |
{ | |
Write-Log "Processing folder: $($baseFolder.DisplayName)" | |
$folderDetail = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$baseFolder.Id) | |
if ($folderDetail.TotalCount) | |
{ | |
Write-Log "Found $($folderDetail.TotalCount) items in folder $($folderDetail.DisplayName)" | |
$Global:totalFolderCount += $folderDetail.TotalCount | |
} | |
Process-Items $baseFolder | |
# Check for and recurse through all sub-folders | |
if ($baseFolder.ChildFolderCount -gt 0 -and $Recursive -eq $true) | |
{ | |
Write-Log "Found $($baseFolder.ChildFolderCount) sub-folders..." | |
$folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView($baseFolder.ChildFolderCount) | |
$propSet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties) | |
$folderView.PropertySet = $propSet | |
$findFolderResults = $exchangeService.FindFolders($baseFolder.Id,$folderView) | |
if ($findFolderResults.TotalCount -gt 0) | |
{ | |
foreach ($folder in $findFolderResults.Folders) | |
{ | |
Find-AllFolders $folder | |
} | |
} | |
else | |
{ | |
Write-Log "Error Folder ($folderId) Not Found" | |
} | |
} | |
} | |
Function FindTargetFolder($FolderPath) | |
{ | |
$targetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot) | |
$pfArray = $FolderPath.Split("\") | |
Write-Log "root: $($targetFolder.DisplayName)" | |
for ($i = 0; $i -lt $pfArray.Length; $i++) | |
{ | |
Write-Log "Testing: $($pfArray[$i])" | |
$folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) | |
$searchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$pfArray[$i]) | |
$propSet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties) | |
$folderView.PropertySet = $propSet | |
$findFolderResults = $exchangeService.FindFolders($targetFolder.Id,$searchFilter,$folderView) | |
if ($findFolderResults.TotalCount -gt 0) | |
{ | |
foreach($folder in $findFolderResults.Folders) | |
{ | |
$targetFolder = $folder | |
} | |
} | |
else | |
{ | |
throw "Error Folder Not Found" | |
} | |
} | |
Write-Log "found target folder: $($targetFolder.displayname)" | |
$targetFolder | |
} | |
Function Write-Log($message, $writeToScreen = $true) | |
{ | |
if ($writeToScreen) | |
{ | |
Write-Host $message | |
} | |
$message = "$(Get-Date -Format s),$message" | |
Add-Content $log $message | |
} | |
#endregion | |
#region Fields | |
$maxFolderBatchSize = 1000 | |
$maxItemBatchSize = 100 | |
$totalDeletedItems = 0 | |
$deleteMode = "SoftDelete" | |
#endregion | |
#region Main | |
if(!$PSScriptRoot) | |
{ | |
$PSScriptRoot = Split-Path $Script:MyInvocation.MyCommand.Path -Parent | |
} | |
$log = "$PSScriptRoot\Log_$((Get-Date).Ticks).txt" | |
New-Item $log -Type File | Out-Null | |
Write-Log "-whatIf is set to $whatIf" | |
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } ; | |
[Void][Reflection.Assembly]::LoadFile($EwsDllPath) | |
$exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::$ExchangeVersion) | |
$exchangeService.UseDefaultCredentials = $true | |
$exchangeService.Url = $EwsUrl | |
try | |
{ | |
$rootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService, [Microsoft.Exchange.WebServices.Data.WellknownFolderName]::PublicFoldersRoot) | |
} | |
catch | |
{ | |
throw "Error: Can't access mailbox information store. No duplicates were removed from the '$emailAddress' mailbox." | |
} | |
Write-Log "Binding to $FolderPath" | |
$targetFolder = FindTargetFolder($FolderPath) | |
Find-AllFolders $targetFolder | |
#endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment