This is the first of a number of samples I'll be posting (in no specific order) from the talk I gave at MEC thanks to all those that did brave my talk and all the other people I got to talk with about EWS over the conference. For me MEC the missing conference was really unmissable and would recommend if you didn't catch it this time look out for it in the future also check out the new community site iammec.
So let's look at the method this script uses to detect duplicates, firstly there are a number of way that dups can be created and the method this script uses won't work in every situation so it's important to understand how it works. The primary Extended Property this script uses to detect duplicate Items is the pidTagSearchKey (or PR_Search_Key) http://msdn.microsoft.com/en-us/library/office/cc815908.aspx . If you read this link or your a fan of using a MAPI editor like me you would know that this property is used on both the Item and on the recipients in the recipients collection. The reason this property is useful for finding duplicates is that its "Unique in the Entire World" and doesn't change during a copy operation. So for cases where users have incorrectly copied Items (folder to folder, pst to mailbox, mailbox to archive) or they have been copied/Imported by some other method (and there are many) this should work. Where it won't work is when you just have a rouge app or piece of code that creates Items because each newly created Item in this case would have a unique SearchKey in that case you'll need to pick another property you want to use. As a secondary property this script uses the ReceivedTime which also shouldn't change when you copy an Item. The other thing to be careful of with this method is if the duplicates have been around for a while and you have changes being made to one of the duplicates and not the other. This script won't delete any duplicates it finds within a folder instead it will create a folder in the DeletedItems folder called detected duplicates with the datetime the script ran and it will then copy those detected duplicates to this folder. So if all goes wrong you have the ability to copy the items back otherwise they will get deleted when the user empties their deletedItems folder (which maybe never).
This script has one other new piece of code that I came up with for MEC which is the folder selector code snipit so unlike a lot of the scripts I've shown before where you need to set the Mailbox folder you want to work with or use some code to find that folder if its not a well-known folder when you run this script it will first do a findfolder operation to grab all the folders in a mailbox. It will then build a TreeView of all the Mailbox Folders and then present a GUI that will allow you to select what mailbox folder you want to work with.eg
Then the rest of the code will run based on the doubleclick event on the particular leaf you select and doubleclick. The EWS FolderId is stored and retrieved from the tag property of each leaf of the treeview. I've latched this script meaning you need to confirm every duplicate it finds but I've also now added an all option which means if you don't want to have to confirm every Item you can just say yes once.
To run this script just pass in the primary SMTPAddress of the mailbox you want to run it against and it will prompt for the credentials. eg .\finddups.ps1 mec@msgdevelop.onmicrosoft.com
I've put a download copy of the script here the code itself look like
So let's look at the method this script uses to detect duplicates, firstly there are a number of way that dups can be created and the method this script uses won't work in every situation so it's important to understand how it works. The primary Extended Property this script uses to detect duplicate Items is the pidTagSearchKey (or PR_Search_Key) http://msdn.microsoft.com/en-us/library/office/cc815908.aspx . If you read this link or your a fan of using a MAPI editor like me you would know that this property is used on both the Item and on the recipients in the recipients collection. The reason this property is useful for finding duplicates is that its "Unique in the Entire World" and doesn't change during a copy operation. So for cases where users have incorrectly copied Items (folder to folder, pst to mailbox, mailbox to archive) or they have been copied/Imported by some other method (and there are many) this should work. Where it won't work is when you just have a rouge app or piece of code that creates Items because each newly created Item in this case would have a unique SearchKey in that case you'll need to pick another property you want to use. As a secondary property this script uses the ReceivedTime which also shouldn't change when you copy an Item. The other thing to be careful of with this method is if the duplicates have been around for a while and you have changes being made to one of the duplicates and not the other. This script won't delete any duplicates it finds within a folder instead it will create a folder in the DeletedItems folder called detected duplicates with the datetime the script ran and it will then copy those detected duplicates to this folder. So if all goes wrong you have the ability to copy the items back otherwise they will get deleted when the user empties their deletedItems folder (which maybe never).
This script has one other new piece of code that I came up with for MEC which is the folder selector code snipit so unlike a lot of the scripts I've shown before where you need to set the Mailbox folder you want to work with or use some code to find that folder if its not a well-known folder when you run this script it will first do a findfolder operation to grab all the folders in a mailbox. It will then build a TreeView of all the Mailbox Folders and then present a GUI that will allow you to select what mailbox folder you want to work with.eg
Then the rest of the code will run based on the doubleclick event on the particular leaf you select and doubleclick. The EWS FolderId is stored and retrieved from the tag property of each leaf of the treeview. I've latched this script meaning you need to confirm every duplicate it finds but I've also now added an all option which means if you don't want to have to confirm every Item you can just say yes once.
To run this script just pass in the primary SMTPAddress of the mailbox you want to run it against and it will prompt for the credentials. eg .\finddups.ps1 mec@msgdevelop.onmicrosoft.com
I've put a download copy of the script here the code itself look like
- ## Get the Mailbox to Access from the 1st commandline argument
- $MailboxName = $args[0]
- ## Load Managed API dll
- Add-Type -Path "C:\Program Files\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"
- [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
- [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
- ## Set Exchange Version
- $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
- ## Create Exchange Service Object
- $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
- ## Set Credentials to use two options are availible Option1 to use explict credentials or Option 2 use the Default (logged On) credentials
- #Credentials Option 1 using UPN for the windows Account
- $psCred = Get-Credential
- $creds = New-Object System.Net.NetworkCredential($psCred.UserName.ToString(),$psCred.GetNetworkCredential().password.ToString())
- $service.Credentials = $creds
- $service.EnableScpLookup = $false
- #Credentials Option 2
- #service.UseDefaultCredentials = $true
- ## Choose to ignore any SSL Warning issues caused by Self Signed Certificates
- ## Code From http://poshcode.org/624
- ## Create a compilation environment
- $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
- $Compiler=$Provider.CreateCompiler()
- $Params=New-Object System.CodeDom.Compiler.CompilerParameters
- $Params.GenerateExecutable=$False
- $Params.GenerateInMemory=$True
- $Params.IncludeDebugInformation=$False
- $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null
- $TASource=@'
- namespace Local.ToolkitExtensions.Net.CertificatePolicy{
- public class TrustAll : System.Net.ICertificatePolicy {
- public TrustAll() {
- }
- public bool CheckValidationResult(System.Net.ServicePoint sp,
- System.Security.Cryptography.X509Certificates.X509Certificate cert,
- System.Net.WebRequest req, int problem) {
- return true;
- }
- }
- }
- '@
- $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
- $TAAssembly=$TAResults.CompiledAssembly
- ## We now create an instance of the TrustAll and attach it to the ServicePointManager
- $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
- [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll
- ## end code from http://poshcode.org/624
- ## Set the URL of the CAS (Client Access Server) to use two options are availbe to use Autodiscover to find the CAS URL or Hardcode the CAS to use
- #CAS URL Option 1 Autodiscover
- $service.AutodiscoverUrl($MailboxName,{$true})
- "Using CAS Server : " + $Service.url
- #CAS URL Option 2 Hardcoded
- #$uri=[system.URI] "https://casservername/ews/exchange.asmx"
- #$service.Url = $uri
- ## Optional section for Exchange Impersonation
- #$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName)
- #Define Extended properties
- $PR_FOLDER_TYPE = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(13825,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);
- $folderidcnt = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)
- # Bind to the Contacts Folder
- $rfRootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderidcnt)
- #Define the FolderView used for Export should not be any larger then 1000 folders due to throttling
- $fvFolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
- #Deep Transval will ensure all folders in the search path are returned
- $fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep;
- $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
- $PR_Folder_Path = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(26293, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String);
- #Add Properties to the Property Set
- $psPropertySet.Add($PR_Folder_Path);
- $fvFolderView.PropertySet = $psPropertySet;
- #The Search filter will exclude any Search Folders
- $sfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($PR_FOLDER_TYPE,"1")
- $fiResult = $null
- #
- $Treeinfo = @{}
- $TNRoot = new-object System.Windows.Forms.TreeNode("Root")
- $TNRoot.Name = "Mailbox"
- $TNRoot.Text = "Mailbox - " + $MailboxName
- #The Do loop will handle any paging that is required if there are more the 1000 folders in a mailbox
- do {
- $fiResult = $Service.FindFolders($folderidcnt,$sfSearchFilter,$fvFolderView)
- foreach($ffFolder in $fiResult.Folders){
- #Process folder here
- $TNChild = new-object System.Windows.Forms.TreeNode($ffFolder.DisplayName.ToString())
- $TNChild.Name = $ffFolder.DisplayName.ToString()
- $TNChild.Text = $ffFolder.DisplayName.ToString()
- $TNChild.tag = $ffFolder.Id.UniqueId.ToString()
- if ($ffFolder.ParentFolderId.UniqueId -eq $rfRootFolder.Id.UniqueId ){
- $ffFolder.DisplayName
- [void]$TNRoot.Nodes.Add($TNChild)
- $Treeinfo.Add($ffFolder.Id.UniqueId.ToString(),$TNChild)
- }
- else{
- $pfFolder = $Treeinfo[$ffFolder.ParentFolderId.UniqueId.ToString()]
- [void]$pfFolder.Nodes.Add($TNChild)
- if ($Treeinfo.ContainsKey($ffFolder.Id.UniqueId.ToString()) -eq $false){
- $Treeinfo.Add($ffFolder.Id.UniqueId.ToString(),$TNChild)
- }
- }
- }
- $fvFolderView.Offset += $fiResult.Folders.Count
- }while($fiResult.MoreAvailable -eq $true)
- $Script:clickedFolder = $null
- $objForm = New-Object System.Windows.Forms.Form
- $objForm.Text = "Folder Select Form"
- $objForm.Size = New-Object System.Drawing.Size(600,600)
- $objForm.StartPosition = "CenterScreen"
- $tvTreView1 = new-object System.Windows.Forms.TreeView
- $tvTreView1.Location = new-object System.Drawing.Size(1,1)
- $tvTreView1.add_DoubleClick({
- $Script:clickedFolder = $this.SelectedNode.tag
- $objForm.Close()
- })
- $tvTreView1.size = new-object System.Drawing.Size(580,580)
- $tvTreView1.Anchor = "Top,left,Bottom"
- [void]$tvTreView1.Nodes.Add($TNRoot)
- $objForm.controls.add($tvTreView1)
- $objForm.ShowDialog()
- $clickedfolderid = new-object Microsoft.Exchange.WebServices.Data.FolderId($Script:clickedFolder)
- $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::DeletedItems,$MailboxName)
- $DuplicatesFolder = New-Object Microsoft.Exchange.WebServices.Data.Folder -ArgumentList $service
- $DuplicatesFolder.DisplayName = "DuplicateItems-Deduped-" + (Get-Date).ToString("yyyy-MM-dd-hh-mm-ss")
- $DuplicatesFolder.Save($folderid)
- #Define ItemView to retrive just 1000 Items
- $PidTagSearchKey = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x300B, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)
- $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
- $psPropset.add($PidTagSearchKey)
- $dupHash = @{}
- #Create Collection for Move Batch
- $Itemids = @()
- $script:allChoice = $false
- $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
- $ivItemView.PropertySet = $psPropset
- $fiItems = $null
- do{
- $fiItems = $service.FindItems($clickedfolderid,$ivItemView)
- #[Void]$service.LoadPropertiesForItems($fiItems,$psPropset)
- foreach($Item in $fiItems.Items){
- $PropVal = $null
- if($Item.TryGetProperty($PidTagSearchKey,[ref]$PropVal)){
- $SearchString = [System.BitConverter]::ToString($PropVal).Replace("-","")
- if($dupHash.ContainsKey($SearchString)){
- #Check the recivedDate if availible
- if($Item.DateTimeReceived -ne $null){
- if($Item.DateTimeReceived -eq $dupHash[$SearchString]){
- if($script:allChoice -eq $false){
- $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes",""
- $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No",""
- $all = new-Object System.Management.Automation.Host.ChoiceDescription "&All","";
- $choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no,$all)
- $message = "Duplicated Detected : Subject " + $Item.Subject + " : Received-" + $dupHash[$SearchString] + " : Created-" + $Item.DateTimeCreated
- $result = $Host.UI.PromptForChoice($caption,$message,$choices,0)
- if($result -eq 0) {
- $Itemids += $Item
- }
- else{
- if($result -eq 2){
- $script:allChoice = $true
- $Itemids += $Item
- }
- }
- }
- else{
- $Itemids += $Item
- }
- }
- }else{
- "Duplicate Found : " + $Item.Subject
- $Itemids += $Item
- }
- }
- else{
- "Procesing Item " + $Item.Subject
- if($Item.DateTimeReceived -ne $null){
- $dupHash.add($SearchString,$Item.DateTimeReceived)
- }
- else{
- $dupHash.add($SearchString,"")
- }
- }
- }
- }
- $ivItemView.Offset += $fiItems.Items.Count
- }while($fiItems.MoreAvailable -eq $true)
- #Total Items Processed Varible
- $nmbProcessed = 0
- if($Itemids.Count -gt 0){
- write-host ("Move " + $Itemids.Count + " Items")
- #Create Collection for Move Batch
- $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
- $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.ItemId" -as "Type")
- $BatchItemids = [Activator]::CreateInstance($type)
- #Varible to Track BatchSize
- $batchSize = 0
- foreach($iiID in $Itemids){
- $nmbProcessed++
- $BatchItemids.Add($iiID.Id)
- if($iiID.Size -ne $null){
- $batchSize += $iiID.Size
- }
- #if BatchCount greator then 50 or larger the 10 MB Move Batch
- if($BatchItemids.Count -eq 50 -bor $batchSize -gt (10*1MB)){
- $Result = $null
- $Result = $service.MoveItems($BatchItemids,$DuplicatesFolder.Id)
- [INT]$collectionCount = 0
- [INT]$Rcount = 0
- [INT]$Errcount = 0
- $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
- $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.ItemId" -as "Type")
- #Define Collection to Retry Move For faild Items
- if($Result -ne $null){
- foreach ($res in $Result){
- if ($res.Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success){
- $Rcount++
- }
- else{
- $Errcount++
- }
- $collectionCount++
- }
- }
- else{
- Write-Host -foregroundcolor red ("Move Result Null Exception")
- }
- Write-host ($Rcount.ToString() + " Items Moved Successfully " + "Total Processed " + $nmbProcessed + " Total Folder Items " + $Itemids.Count)
- if($Errcount -gt 0){
- Write-Host -foregroundcolor red ($Errcount.ToString() + " Error failed Moved")
- }
- $BatchItemids.Clear()
- $batchSize = 0
- }
- }
- if($BatchItemids.Count -gt 0){
- $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
- $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.ItemId" -as "Type")
- $RetryBatchItemids = [Activator]::CreateInstance($type)
- $Result = $service.MoveItems($BatchItemids,$DuplicatesFolder.Id)
- [INT]$Rcount = 0
- [INT]$Errcount = 0
- foreach ($res in $Result){
- if ($res.Result -eq [Microsoft.Exchange.WebServices.Data.ServiceResult]::Success){
- $Rcount++
- }
- else{
- $Errcount++
- }
- }
- Write-host ($Rcount.ToString() + " Items Moved Successfully")
- if($Errcount -gt 0){
- Write-Host -foregroundcolor red ($Errcount.ToString() + " Error failed Moved")
- }
- }
- }
- $DuplicatesFolder.Load()
- if($DuplicatesFolder.TotalCount -eq 0){
- $DuplicatesFolder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
- }