Skip to content

Instantly share code, notes, and snippets.

@blakeja
Last active July 13, 2017 02:29
Show Gist options
  • Save blakeja/0291e824ff92258aa62566109579a7d4 to your computer and use it in GitHub Desktop.
Save blakeja/0291e824ff92258aa62566109579a7d4 to your computer and use it in GitHub Desktop.
Remove duplicate items in Exchange public folders
#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