From 8e656a0876aed809aa3a5c7dbf51f7b4cd78a374 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 8 Apr 2021 16:15:38 -0500 Subject: [PATCH 1/9] Reorganize --- PublicFolders/{ => src}/RemoveInvalidPermissions.ps1 | 0 PublicFolders/{ => src}/SourceSideValidations/.gitignore | 0 .../{ => src}/SourceSideValidations/Get-BadDumpsterMappings.ps1 | 0 .../{ => src}/SourceSideValidations/Get-BadPermission.ps1 | 0 .../{ => src}/SourceSideValidations/Get-BadPermissionJob.ps1 | 0 PublicFolders/{ => src}/SourceSideValidations/Get-IpmSubtree.ps1 | 0 PublicFolders/{ => src}/SourceSideValidations/Get-ItemCount.ps1 | 0 .../{ => src}/SourceSideValidations/Get-LimitsExceeded.ps1 | 0 .../{ => src}/SourceSideValidations/Get-NonIpmSubtree.ps1 | 0 PublicFolders/{ => src}/SourceSideValidations/JobQueue.ps1 | 0 .../{ => src}/SourceSideValidations/SourceSideValidations.ps1 | 0 PublicFolders/{ => src}/ValidateMailEnabledPublicFolders.ps1 | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename PublicFolders/{ => src}/RemoveInvalidPermissions.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/.gitignore (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-BadDumpsterMappings.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-BadPermission.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-BadPermissionJob.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-IpmSubtree.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-ItemCount.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-LimitsExceeded.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/Get-NonIpmSubtree.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/JobQueue.ps1 (100%) rename PublicFolders/{ => src}/SourceSideValidations/SourceSideValidations.ps1 (100%) rename PublicFolders/{ => src}/ValidateMailEnabledPublicFolders.ps1 (100%) diff --git a/PublicFolders/RemoveInvalidPermissions.ps1 b/PublicFolders/src/RemoveInvalidPermissions.ps1 similarity index 100% rename from PublicFolders/RemoveInvalidPermissions.ps1 rename to PublicFolders/src/RemoveInvalidPermissions.ps1 diff --git a/PublicFolders/SourceSideValidations/.gitignore b/PublicFolders/src/SourceSideValidations/.gitignore similarity index 100% rename from PublicFolders/SourceSideValidations/.gitignore rename to PublicFolders/src/SourceSideValidations/.gitignore diff --git a/PublicFolders/SourceSideValidations/Get-BadDumpsterMappings.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadDumpsterMappings.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-BadDumpsterMappings.ps1 rename to PublicFolders/src/SourceSideValidations/Get-BadDumpsterMappings.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-BadPermission.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadPermission.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-BadPermission.ps1 rename to PublicFolders/src/SourceSideValidations/Get-BadPermission.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-BadPermissionJob.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadPermissionJob.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-BadPermissionJob.ps1 rename to PublicFolders/src/SourceSideValidations/Get-BadPermissionJob.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-IpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-IpmSubtree.ps1 rename to PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-ItemCount.ps1 b/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-ItemCount.ps1 rename to PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-LimitsExceeded.ps1 b/PublicFolders/src/SourceSideValidations/Get-LimitsExceeded.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-LimitsExceeded.ps1 rename to PublicFolders/src/SourceSideValidations/Get-LimitsExceeded.ps1 diff --git a/PublicFolders/SourceSideValidations/Get-NonIpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/Get-NonIpmSubtree.ps1 rename to PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 diff --git a/PublicFolders/SourceSideValidations/JobQueue.ps1 b/PublicFolders/src/SourceSideValidations/JobQueue.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/JobQueue.ps1 rename to PublicFolders/src/SourceSideValidations/JobQueue.ps1 diff --git a/PublicFolders/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 similarity index 100% rename from PublicFolders/SourceSideValidations/SourceSideValidations.ps1 rename to PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 diff --git a/PublicFolders/ValidateMailEnabledPublicFolders.ps1 b/PublicFolders/src/ValidateMailEnabledPublicFolders.ps1 similarity index 100% rename from PublicFolders/ValidateMailEnabledPublicFolders.ps1 rename to PublicFolders/src/ValidateMailEnabledPublicFolders.ps1 From 039984189f18ebe2e2132cc9b66eb64016b6b750 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 8 Apr 2021 16:25:42 -0500 Subject: [PATCH 2/9] Make RemoveInvalidPermissions a switch instead of separate script --- .../src/RemoveInvalidPermissions.ps1 | 21 -------------- .../Remove-InvalidPermissions.ps1 | 28 +++++++++++++++++++ .../SourceSideValidations.ps1 | 24 ++++++++++++---- 3 files changed, 47 insertions(+), 26 deletions(-) delete mode 100644 PublicFolders/src/RemoveInvalidPermissions.ps1 create mode 100644 PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 diff --git a/PublicFolders/src/RemoveInvalidPermissions.ps1 b/PublicFolders/src/RemoveInvalidPermissions.ps1 deleted file mode 100644 index 22359e9626..0000000000 --- a/PublicFolders/src/RemoveInvalidPermissions.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -param($csvFile) - -$badPermissions = Import-Csv $csvFile -$byEntryId = $badPermissions | Group-Object EntryId -$progressCount = 0 -$badPermissions | Select-Object -Unique EntryId | ForEach-Object { - $progressCount++ - Write-Progress -Activity "Removing invalid permissions" -Status "$progressCount / $($byEntryId.Count)" -PercentComplete ($progressCount * 100 / $byEntryId.Count) -CurrentOperation $_.Identity - $folder = $_ - Get-PublicFolderClientPermission -Identity $folder.EntryId | ForEach-Object { - if ( - ($_.User.DisplayName -ne "Default") -and - ($_.User.DisplayName -ne "Anonymous") -and - ($null -eq $_.User.ADRecipient) -and - ($_.User.UserType -eq "Unknown") - ) { - Write-Host "Removing $($_.User.DisplayName) from folder $($_.Identity.ToString())" - $_ | Remove-PublicFolderClientPermission -Confirm:$false - } - } -} \ No newline at end of file diff --git a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 new file mode 100644 index 0000000000..3491fcc47c --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 @@ -0,0 +1,28 @@ +function Remove-InvalidPermission { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $CsvFile + ) + + $badPermissions = Import-Csv $csvFile + $byEntryId = $badPermissions | Group-Object EntryId + $progressCount = 0 + $badPermissions | Select-Object -Unique EntryId | ForEach-Object { + $progressCount++ + Write-Progress -Activity "Removing invalid permissions" -Status "$progressCount / $($byEntryId.Count)" -PercentComplete ($progressCount * 100 / $byEntryId.Count) -CurrentOperation $_.Identity + $folder = $_ + Get-PublicFolderClientPermission -Identity $folder.EntryId | ForEach-Object { + if ( + ($_.User.DisplayName -ne "Default") -and + ($_.User.DisplayName -ne "Anonymous") -and + ($null -eq $_.User.ADRecipient) -and + ($_.User.UserType -eq "Unknown") + ) { + Write-Host "Removing $($_.User.DisplayName) from folder $($_.Identity.ToString())" + $_ | Remove-PublicFolderClientPermission -Confirm:$false + } + } + } +} diff --git a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 index 53fa217118..625155908d 100644 --- a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 +++ b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 @@ -1,8 +1,16 @@ -[CmdletBinding()] +[CmdletBinding(DefaultParameterSetName = "Default")] param ( - [Parameter()] + [Parameter(Mandatory = $false, ParameterSetName = "Default")] [bool] - $StartFresh = $true + $StartFresh = $true, + + [Parameter(Mandatory = $true, ParameterSetName = "RemoveInvalidPermissions")] + [Switch] + $RemoveInvalidPermissions, + + [Parameter(Mandatory = $true, ParameterSetName = "RemoveInvalidPermissions")] + [string] + $CsvFile ) . $PSScriptRoot\Get-IpmSubtree.ps1 @@ -13,6 +21,12 @@ param ( . $PSScriptRoot\Get-BadPermission.ps1 . $PSScriptRoot\Get-BadPermissionJob.ps1 . $PSScriptRoot\JobQueue.ps1 +. $PSScriptRoot\Remove-InvalidPermission.ps1 + +if ($RemoveInvalidPermissions) { + Remove-InvalidPermission -CsvFile $CsvFile + return +} $startTime = Get-Date @@ -152,8 +166,8 @@ if ($badPermissions.Count -gt 0) { Write-Host Write-Host $badPermissions.Count "invalid permissions were found. These are listed in the following CSV file:" Write-Host $badPermissionsFile -ForegroundColor Green - Write-Host "The invalid permissions can be removed using the RemoveInvalidPermissions script as follows:" - Write-Host ".\RemoveInvalidPermissions.ps1 $badPermissionsFile" + Write-Host "The invalid permissions can be removed using the RemoveInvalidPermissions switch as follows:" + Write-Host ".\SourceSideValidations.ps1 -RemoveInvalidPermissions -CsvFile $badPermissionsFile" } $folderCountMigrationLimit = 250000 From 6b918ecaafe5ea6b169551b1ecd5342ae024d2bd Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 09:57:02 -0500 Subject: [PATCH 3/9] Make mail enabled validation part of SourceSideValidations --- .../Get-BadMailEnabledFolder.ps1 | 172 ++++++++++++++++++ .../SourceSideValidations/Get-IpmSubtree.ps1 | 22 ++- .../Get-NonIpmSubtree.ps1 | 5 +- ...sions.ps1 => Remove-InvalidPermission.ps1} | 4 +- .../SourceSideValidations.ps1 | 92 +++++++++- 5 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 rename PublicFolders/src/SourceSideValidations/{Remove-InvalidPermissions.ps1 => Remove-InvalidPermission.ps1} (91%) diff --git a/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 new file mode 100644 index 0000000000..e0c21aebf0 --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 @@ -0,0 +1,172 @@ +function Get-BadMailEnabledFolder { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param ( + [Parameter()] + [PSCustomObject] + $FolderData + ) + + begin { + $startTime = Get-Date + $progressCount = 0 + $sw = New-Object System.Diagnostics.Stopwatch + $sw.Start() + $progressParams = @{ + Activity = "Validating mail-enabled public folders" + Id = 2 + ParentId = 1 + } + } + + process { + $nonIpmSubtreeMailEnabled = @($folderData.NonIpmSubtree | Where-Object { $_.MailEnabled }) + $ipmSubtreeMailEnabled = @($folderData.IpmSubtree | Where-Object { $_.MailEnabled }) + $mailDisabledWithProxyGuid = @($folderData.IpmSubtree | Where-Object { -not $_.MailEnabled -and $null -ne $_.MailRecipientGuid -and [Guid]::Empty -ne $_.MailRecipientGuid } | ForEach-Object { $_.Identity.ToString() }) + + + $mailEnabledFoldersWithNoADObject = @() + $mailPublicFoldersLinked = New-Object 'System.Collections.Generic.Dictionary[string, object]' + $progressParams.Activity = "Checking for missing AD objects" + for ($i = 0; $i -lt $ipmSubtreeMailEnabled.Count; $i++) { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -PercentComplete ($i * 100 / $ipmSubtreeMailEnabled.Count) -Status ("$i of $($ipmSubtreeMailEnabled.Count)") + } + $result = $ipmSubtreeMailEnabled[$i] | Get-MailPublicFolder -ErrorAction SilentlyContinue + if ($null -eq $result) { + $mailEnabledFoldersWithNoADObject += $ipmSubtreeMailEnabled[$i] + } else { + $guidString = $result.Guid.ToString() + if (-not $mailPublicFoldersLinked.ContainsKey($guidString)) { + $mailPublicFoldersLinked.Add($guidString, $result) | Out-Null + } + } + } + + $progressCount = 0 + $progressParams.Activity = "Getting all MailPublicFolder objects" + $allMailPublicFolders = @(Get-MailPublicFolder -ResultSize Unlimited | ForEach-Object { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -Status "$i" + } + }) + + + $progressCount = 0 + $progressParams.Activity = "Checking for orphaned MailPublicFolders" + $orphanedMailPublicFolders = @($allMailPublicFolders | ForEach-Object { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -PercentComplete ($progressCount * 100 / $allMailPublicFolders.Count) -Status ("$i of $($allMailPublicFolders.Count)") + } + + if (!($mailPublicFoldersLinked.ContainsKey($_.Guid.ToString()))) { + $orphanedMailPublicFolders += $_ + } + }) + + + $progressParams.Activity = "Building EntryId HashSets" + Write-Progress @progressParams + $byEntryId = New-Object 'System.Collections.Generic.Dictionary[string, object]' + $FolderData.IpmSubtree | ForEach-Object { $byEntryId.Add($_.EntryId.ToString(), $_) } + $byPartialEntryId = New-Object 'System.Collections.Generic.Dictionary[string, object]' + $FolderData.IpmSubtree | ForEach-Object { $byPartialEntryId.Add($_.EntryId.ToString().Substring(44), $_) } + + + $orphanedMPFsThatPointToAMailDisabledFolder = @() + $orphanedMPFsThatPointToAMailEnabledFolder = @() + $orphanedMPFsThatPointToNothing = @() + $emailAddressMergeCommands = @() + $progressParams.Activity = "Checking for orphans that point to a valid folder" + for ($i = 0; $i -lt $orphanedMailPublicFolders.Count; $i++) { + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -PercentComplete ($i * 100 / $orphanedMailPublicFolders.Count) -Status ("$i of $($orphanedMailPublicFolders.Count)") + } + + $thisMPF = $orphanedMailPublicFolders[$i] + $pf = $null + if ($null -ne $thisMPF.ExternalEmailAddress -and $thisMPF.ExternalEmailAddress.ToString().StartsWith("expf")) { + $partialEntryId = $thisMPF.ExternalEmailAddress.ToString().Substring(5).Replace("-", "") + $partialEntryId += "0000" + if ($byPartialEntryId.TryGetValue($partialEntryId, [ref]$pf)) { + if ($pf.MailEnabled) { + + $command = GetCommandToMergeEmailAddresses $pf $thisMPF + if ($null -ne $command) { + $emailAddressMergeCommands += $command + } + + $orphanedMPFsThatPointToAMailEnabledFolder += $thisMPF + } else { + $orphanedMPFsThatPointToAMailDisabledFolder += $thisMPF + } + + continue + } + } + + if ($null -ne $thisMPF.EntryId -and $byEntryId.TryGetValue($thisMPF.EntryId.ToString(), [ref]$pf)) { + if ($pf.MailEnabled) { + + $command = GetCommandToMergeEmailAddresses $pf $thisMPF + if ($null -ne $command) { + $emailAddressMergeCommands += $command + } + + $orphanedMPFsThatPointToAMailEnabledFolder += $thisMPF + } else { + $orphanedMPFsThatPointToAMailDisabledFolder += $thisMPF + } + } else { + $orphanedMPFsThatPointToNothing += $thisMPF + } + } + } + + end { + Write-Verbose "$($ipmSubtreeMailEnabled.Count) public folders are mail-enabled." + Write-Verbose "$($mailPublicFoldersLinked.Keys.Count) folders are mail-enabled and are properly linked to an existing AD object." + Write-Verbose "$($nonIpmSubtreeMailEnabled.Count) System folders are mail-enabled." + Write-Verbose "$($mailEnabledFoldersWithNoADObject.Count) folders are mail-enabled with no AD object." + Write-Verbose "$($orphanedMailPublicFolders.Count) MailPublicFolders are orphaned." + Write-Verbose "$($orphanedMPFsThatPointToAMailEnabledFolder.Count) of those orphans point to mail-enabled folders that point to some other object." + Write-Verbose "$($orphanedMPFsThatPointToAMailDisabledFolder.Count) of those orphans point to mail-disabled folders." + + $foldersToMailDisable = @() + $nonIpmSubtreeMailEnabled | ForEach-Object { $foldersToMailDisable += $_.Identity.ToString() } + $mailEnabledFoldersWithNoADObject | ForEach-Object { $foldersToMailDisable += $_.Identity } + + [PSCustomObject]@{ + FoldersToMailDisable = $foldersToMailDisable + MailPublicFoldersToDelete = $orphanedMPFsThatPointToNothing | ForEach-Object { $_.DistinguishedName.Replace("/", "\/") } + MailPublicFolderDuplicates = $orphanedMPFsThatPointToAMailEnabledFolder | ForEach-Object { $mailPublicFolderDuplicates += $_.DistinguishedName } + EmailAddressMergeCommands = $emailAddressMergeCommands + MailDisabledWithProxyGuid = $mailDisabledWithProxyGuid + MailPublicFoldersDisconnected = $orphanedMPFsThatPointToAMailDisabledFolder | ForEach-Object { $mailPublicFoldersDisconnected += $_.DistinguishedName } + } + + Write-Host "Get-BadMailEnabledFolder duration" ((Get-Date) - $startTime) + } +} + +function GetCommandToMergeEmailAddresses($publicFolder, $orphanedMailPublicFolder) { + $linkedMailPublicFolder = Get-PublicFolder $publicFolder.Identity | Get-MailPublicFolder + $emailAddressesOnGoodObject = @($linkedMailPublicFolder.EmailAddresses | Where-Object { $_.ToString().StartsWith("smtp:", "OrdinalIgnoreCase") } | ForEach-Object { $_.ToString().Substring($_.ToString().IndexOf(':') + 1) }) + $emailAddressesOnBadObject = @($orphanedMailPublicFolder.EmailAddresses | Where-Object { $_.ToString().StartsWith("smtp:", "OrdinalIgnoreCase") } | ForEach-Object { $_.ToString().Substring($_.ToString().IndexOf(':') + 1) }) + $emailAddressesToAdd = $emailAddressesOnBadObject | Where-Object { -not $emailAddressesOnGoodObject.Contains($_) } + $emailAddressesToAdd = $emailAddressesToAdd | ForEach-Object { "`"" + $_ + "`"" } + if ($emailAddressesToAdd.Count -gt 0) { + $emailAddressesToAddString = [string]::Join(",", $emailAddressesToAdd) + $command = "Get-PublicFolder `"$($publicFolder.Identity)`" | Get-MailPublicFolder | Set-MailPublicFolder -EmailAddresses @{add=$emailAddressesToAddString}" + return $command + } else { + return $null + } +} diff --git a/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 index a330999f2a..defd1e0d1c 100644 --- a/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 @@ -26,7 +26,7 @@ $ipmSubtree = Import-Csv $PSScriptRoot\IpmSubtree.csv } else { $ipmSubtree = Get-PublicFolder -Recurse -ResultSize Unlimited | - Select-Object Identity, EntryId, ParentFolder, DumpsterEntryId, FolderPath, FolderSize, HasSubfolders, ContentMailboxName | + Select-Object Identity, EntryId, ParentFolder, DumpsterEntryId, FolderPath, FolderSize, HasSubfolders, ContentMailboxName, MailEnabled, MailRecipientGuid | ForEach-Object { $progressCount++ $currentFolder = $_.Identity.ToString() @@ -37,15 +37,17 @@ } [PSCustomObject]@{ - Identity = $_.Identity.ToString() - EntryId = $_.EntryId.ToString() - ParentEntryId = $_.ParentFolder.ToString() - DumpsterEntryId = if ($_.DumpsterEntryId) { $_.DumpsterEntryId.ToString() } else { $null } - FolderPathDepth = $_.FolderPath.Depth - FolderSize = $_.FolderSize - HasSubfolders = $_.HasSubfolders - ContentMailbox = $_.ContentMailboxName - ItemCount = 0 + Identity = $_.Identity.ToString() + EntryId = $_.EntryId.ToString() + ParentEntryId = $_.ParentFolder.ToString() + DumpsterEntryId = if ($_.DumpsterEntryId) { $_.DumpsterEntryId.ToString() } else { $null } + FolderPathDepth = $_.FolderPath.Depth + FolderSize = $_.FolderSize + HasSubfolders = $_.HasSubfolders + ContentMailbox = $_.ContentMailboxName + MailEnabled = $_.MailEnabled + MailRecipientGuid = $_.MailRecipientGuid + ItemCount = 0 } } catch { $errors++ diff --git a/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 index 2c9bdde09e..2e8104c0a2 100644 --- a/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 @@ -26,12 +26,12 @@ $nonIpmSubtree = Import-Csv $PSScriptRoot\NonIpmSubtree.csv } else { $nonIpmSubtree = Get-PublicFolder \non_ipm_subtree -Recurse -ResultSize Unlimited | - Select-Object Identity, EntryId, DumpsterEntryId | + Select-Object Identity, EntryId, DumpsterEntryId, MailEnabled | ForEach-Object { $progressCount++ $currentFolder = $_.Identity.ToString() try { - # Updating progress too often has a perf impact, so we only update every 100 folders. + # Updating progress too often has a perf impact, so we only update every second. if ($sw.ElapsedMilliseconds -gt 1000) { $sw.Restart() Write-Progress @progressParams -Status $progressCount @@ -41,6 +41,7 @@ Identity = $_.Identity.ToString() EntryId = $_.EntryId.ToString() DumpsterEntryId = if ($_.DumpsterEntryId) { $_.DumpsterEntryId.ToString() } else { $null } + MailEnabled = $_.MailEnabled } } catch { $errors++ diff --git a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 similarity index 91% rename from PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 rename to PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 index 3491fcc47c..e7f9e1f1c4 100644 --- a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermissions.ps1 +++ b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 @@ -1,5 +1,5 @@ function Remove-InvalidPermission { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter()] [string] @@ -21,7 +21,7 @@ function Remove-InvalidPermission { ($_.User.UserType -eq "Unknown") ) { Write-Host "Removing $($_.User.DisplayName) from folder $($_.Identity.ToString())" - $_ | Remove-PublicFolderClientPermission -Confirm:$false + $_ | Remove-PublicFolderClientPermission } } } diff --git a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 index 625155908d..55963da2e0 100644 --- a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 +++ b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding(DefaultParameterSetName = "Default")] +[CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess)] param ( [Parameter(Mandatory = $false, ParameterSetName = "Default")] [bool] @@ -22,6 +22,7 @@ param ( . $PSScriptRoot\Get-BadPermissionJob.ps1 . $PSScriptRoot\JobQueue.ps1 . $PSScriptRoot\Remove-InvalidPermission.ps1 +. $PSScriptRoot\Get-BadMailEnabledFolder.ps1 if ($RemoveInvalidPermissions) { Remove-InvalidPermission -CsvFile $CsvFile @@ -37,7 +38,7 @@ $progressParams = @{ Id = 1 } -Write-Progress @progressParams -Status "Step 1 of 7" +Write-Progress @progressParams -Status "Step 1 of 8" $ipmSubtree = Get-IpmSubtree -startFresh $StartFresh @@ -45,11 +46,11 @@ if ($ipmSubtree.Count -lt 1) { return } -Write-Progress @progressParams -Status "Step 2 of 7" +Write-Progress @progressParams -Status "Step 2 of 8" $nonIpmSubtree = Get-NonIpmSubtree -startFresh $StartFresh -Write-Progress @progressParams -Status "Step 3 of 7" +Write-Progress @progressParams -Status "Step 3 of 8" $hashtableProgress = @{ Activity = "Populating hashtables" @@ -94,26 +95,101 @@ if ($script:anyDatabaseDown) { Write-Progress @hashtableProgress -Completed -Write-Progress @progressParams -Status "Step 4 of 7" +Write-Progress @progressParams -Status "Step 4 of 8" Get-ItemCount -FolderData $FolderData # Now we're ready to do the checks -Write-Progress @progressParams -Status "Step 5 of 7" +Write-Progress @progressParams -Status "Step 5 of 8" $badDumpsters = @(Get-BadDumpsterMappings -FolderData $folderData) -Write-Progress @progressParams -Status "Step 6 of 7" +Write-Progress @progressParams -Status "Step 6 of 8" $limitsExceeded = Get-LimitsExceeded -FolderData $folderData -Write-Progress @progressParams -Status "Step 7 of 7" +Write-Progress @progressParams -Status "Step 7 of 8" + +$badMailEnabled = Get-BadMailEnabledFolder -FolderData $folderData + +Write-Progress @progressParams -Status "Step 8 of 8" $badPermissions = @(Get-BadPermission -FolderData $folderData) # Output the results +if ($badMailEnabled.FoldersToMailDisable.Count -gt 0) { + $foldersToMailDisableFile = Join-Path $PSScriptRoot "FoldersToMailDisable.txt" + Set-Content -Path $foldersToMailDisableFile -Value $badMamilEnabled.FoldersToMailDisable + + Write-Host + Write-Host $badMailEnabled.FoldersToMailDisable.Count "folders should be mail-disabled, either because the MailRecipientGuid" + Write-Host "does not exist, or because they are system folders. These are listed in the file called:" + Write-Host $foldersToMailDisableFile -ForegroundColor Green + Write-Host "After confirming the accuracy of the results, you can mail-disable them with the following command:" + Write-Host "Get-Content `"$foldersToMailDisableFile`" | % { Set-PublicFolder `$_ -MailEnabled `$false }" -ForegroundColor Green +} + +if ($badMailEnabled.MailPublicFoldersToDelete.Count -gt 0) { + $mailPublicFoldersToDeleteFile = Join-Path $PSScriptRoot "MailPublicFolderOrphans.txt" + Set-Content -Path $mailPublicFoldersToDeleteFile -Value $badMailEnabled.MailPublicFoldersToDelete + + Write-Host + Write-Host $badMailEnabled.MailPublicFoldersToDelete.Count "MailPublicFolders are orphans and should be deleted. They exist in Active Directory" + Write-Host "but are not linked to any public folder. These are listed in a file called:" + Write-Host $mailPublicFoldersToDeleteFile -ForegroundColor Green + Write-Host "After confirming the accuracy of the results, you can delete them with the following command:" + Write-Host "Get-Content `"$mailPublicFoldersToDeleteFile`" | % { `$folder = ([ADSI](`"LDAP://`$_`")); `$parent = ([ADSI]`"`$(`$folder.Parent)`"); `$parent.Children.Remove(`$folder) }" -ForegroundColor Green +} + +if ($badMailEnabled.MailPublicFolderDuplicates.Count -gt 0) { + $mailPublicFolderDuplicatesFile = Join-Path $PSScriptRoot "MailPublicFolderDuplicates.txt" + Set-Content -Path $mailPublicFolderDuplicatesFile -Value $badMailEnabled.MailPublicFolderDuplicates + + Write-Host + Write-Host $badMailEnabled.MailPublicFolderDuplicates.Count "MailPublicFolders are duplicates and should be deleted. They exist in Active Directory" + Write-Host "and point to a valid folder, but that folder points to some other directory object." + Write-Host "These are listed in a file called:" + Write-Host $mailPublicFolderDuplicatesFile -ForegroundColor Green + Write-Host "After confirming the accuracy of the results, you can delete them with the following command:" + Write-Host "Get-Content `"$mailPublicFolderDuplicatesFile`" | % { `$folder = ([ADSI](`"LDAP://`$_`")); `$parent = ([ADSI]`"`$(`$folder.Parent)`"); `$parent.Children.Remove(`$folder) }" -ForegroundColor Green + + if ($badMailEnabled.EmailAddressMergeCommands.Count -gt 0) { + $emailAddressMergeScriptFile = Join-Path $PSScriptRoot "AddAddressesFromDuplicates.ps1" + Set-Content -Path $emailAddressMergeScriptFile -Value $badMailEnabled.EmailAddressMergeCommands + Write-Host "The duplicates we are deleting contain email addresses that might still be in use." + Write-Host "To preserve these, we generated a script that will add these to the linked objects for those folders." + Write-Host "After deleting the duplicate objects using the command above, run the script as follows to" + Write-Host "populate these addresses:" + Write-Host ".\$emailAddressMergeScriptFile" -ForegroundColor Green + } +} + +if ($badMailEnabled.MailDisabledWithProxyGuid.Count -gt 0) { + $mailDisabledWithProxyGuidFile = Join-Path $PSScriptRoot "MailDisabledWithProxyGuid.txt" + Set-Content -Path $mailDisabledWithProxyGuidFile -Value $badMailEnabled.MailDisabledWithProxyGuid + + Write-Host + Write-Host $badMailEnabled.MailDisabledWithProxyGuid.Count "public folders have proxy GUIDs even though the folders are mail-disabled." + Write-Host "These folders should be mail-enabled. They can be mail-disabled again afterwards if desired." + Write-Host "To mail-enable these folders, run:" + Write-Host "Get-Content `"$mailDisabledWithProxyGuidFile`" | % { Enable-MailPublicFolder `$_ }" -ForegroundColor Green +} + +if ($badMailEnabled.MailPublicFoldersDisconnected.Count -gt 0) { + $mailPublicFoldersDisconnectedFile = Join-Path $PSScriptRoot "MailPublicFoldersDisconnected.txt" + Set-Content -Path $mailPublicFoldersDisconnectedFile -Value $badMailEnabled.MailPublicFoldersDisconnected + + Write-Host + Write-Host $badMailEnabled.MailPublicFoldersDisconnected.Count "MailPublicFolders are disconnected from their folders. This means they exist in" + Write-Host "Active Directory and the folders are probably functioning as mail-enabled folders," + Write-Host "even while the properties of the public folders themselves say they are not mail-enabled." + Write-Host "This can be complex to fix. Either the directory object should be deleted, or the public folder" + Write-Host "should be mail-enabled, or both. These directory objects are listed in a file called:" + Write-Host $mailPublicFoldersDisconnectedFile -ForegroundColor Green +} + if ($badDumpsters.Count -gt 0) { $badDumpsterFile = Join-Path $PSScriptRoot "BadDumpsterMappings.txt" Set-Content -Path $badDumpsterFile -Value $badDumpsters From 8a152c383669930555c9b421f142c88e493eecd2 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 16:41:49 -0700 Subject: [PATCH 4/9] Make CSV start processing faster --- .../Remove-InvalidPermission.ps1 | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 index e7f9e1f1c4..d47b7d2a4c 100644 --- a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 +++ b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 @@ -6,23 +6,47 @@ function Remove-InvalidPermission { $CsvFile ) - $badPermissions = Import-Csv $csvFile - $byEntryId = $badPermissions | Group-Object EntryId - $progressCount = 0 - $badPermissions | Select-Object -Unique EntryId | ForEach-Object { - $progressCount++ - Write-Progress -Activity "Removing invalid permissions" -Status "$progressCount / $($byEntryId.Count)" -PercentComplete ($progressCount * 100 / $byEntryId.Count) -CurrentOperation $_.Identity - $folder = $_ - Get-PublicFolderClientPermission -Identity $folder.EntryId | ForEach-Object { - if ( - ($_.User.DisplayName -ne "Default") -and - ($_.User.DisplayName -ne "Anonymous") -and - ($null -eq $_.User.ADRecipient) -and - ($_.User.UserType -eq "Unknown") - ) { - Write-Host "Removing $($_.User.DisplayName) from folder $($_.Identity.ToString())" - $_ | Remove-PublicFolderClientPermission + begin { + + $progressParams = @{ + Activity = "Removing invalid permissions" + } + + $sw = New-Object System.Diagnostics.Stopwatch + $sw.Start() + } + + process { + + $badPermissions = Import-Csv $csvFile + $progressCount = 0 + $entryIdsProcessed = New-Object 'System.Collections.Generic.HashSet[string]' + foreach ($permission in $badPermissions) { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -Status "$progressCount / $($badPermissions.Count)" -PercentComplete ($progressCount * 100 / $badPermissions.Count) -CurrentOperation $permission.Identity + } + + if (-not $entryIdsProcessed.Contains($permission.EntryId)) { + $entryIdsProcessed.Add($permission.EntryId) | Out-Null + $permsOnFolder = Get-PublicFolderClientPermission -Identity $permission.EntryId + $permsOnFolder | ForEach-Object { + if ( + ($_.User.DisplayName -ne "Default") -and + ($_.User.DisplayName -ne "Anonymous") -and + ($null -eq $_.User.ADRecipient) -and + ($_.User.UserType -eq "Unknown") + ) { + if ($PSCmdlet.ShouldProcess("$($permission.Identity)", "Remove $($_.User.DisplayName)")) { + Write-Host "Removing $($_.User.DisplayName) from folder $($permission.Identity)" + $_ | Remove-PublicFolderClientPermission -Confirm:$false + } + } + } } } } -} + + end {} +} \ No newline at end of file From 650f51b786f57d2511aa8315ae67392b90073b41 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 19:39:50 -0700 Subject: [PATCH 5/9] Use jobs to retrieve folder data concurrently --- .../SourceSideValidations/Get-FolderData.ps1 | 94 +++++++++++++++++++ .../SourceSideValidations/Get-IpmSubtree.ps1 | 26 ++--- .../SourceSideValidations/Get-ItemCount.ps1 | 44 ++++----- .../Get-NonIpmSubtree.ps1 | 72 ++++++-------- .../SourceSideValidations.ps1 | 50 ++-------- 5 files changed, 154 insertions(+), 132 deletions(-) create mode 100644 PublicFolders/src/SourceSideValidations/Get-FolderData.ps1 diff --git a/PublicFolders/src/SourceSideValidations/Get-FolderData.ps1 b/PublicFolders/src/SourceSideValidations/Get-FolderData.ps1 new file mode 100644 index 0000000000..c4708ea43a --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Get-FolderData.ps1 @@ -0,0 +1,94 @@ +function Get-FolderData { + [CmdletBinding()] + param ( + [Parameter()] + [bool] + $StartFresh = $true + ) + + begin { + $startTime = Get-Date + $serverName = (Get-Mailbox -PublicFolder (Get-OrganizationConfig).RootPublicFolderMailbox.HierarchyMailboxGuid.ToString()).ServerName + $folderData = [PSCustomObject]@{ + IpmSubtree = $null + IpmSubtreeByMailbox = $null + ParentEntryIdCounts = @{} + EntryIdDictionary = @{} + NonIpmSubtree = $null + NonIpmEntryIdDictionary = @{} + MailboxToServerMap = @{} + ItemCounts = @() + } + } + + process { + if (-not $StartFresh -and (Test-Path $PSScriptRoot\IpmSubtree.csv)) { + $folderData.IpmSubtree = Import-Csv $PSScriptRoot\IpmSubtree.csv + $folderData.NonIpmSubtree = Import-Csv $PSScriptRoot\NonIpmSubtree.csv + $folderData.ItemCounts = Import-Csv $PSScriptRoot\ItemCounts.csv + } else { + Add-JobQueueJob @{ + ArgumentList = $serverName + Name = "Get-IpmSubtree" + ScriptBlock = ${Function:Get-IpmSubtree} + } + + Add-JobQueueJob @{ + ArgumentList = $serverName + Name = "Get-NonIpmSubtree" + ScriptBlock = ${Function:Get-NonIpmSubtree} + } + + Add-JobQueueJob @{ + ArgumentList = $serverName + Name = "Get-ItemCount" + ScriptBlock = ${Function:Get-ItemCount} + } + + $completedJobs = Wait-QueuedJob + + foreach ($job in $completedJobs) { + if ($null -ne $job.IpmSubtree) { + $folderData.IpmSubtree = $job.IpmSubtree + $folderData.IpmSubtree | Export-Csv $PSScriptRoot\IpmSubtree.csv + } + + if ($null -ne $job.NonIpmSubtree) { + $folderData.NonIpmSubtree = $job.NonIpmSubtree + $folderData.NonIpmSubtree | Export-Csv $PSScriptRoot\NonIpmSubtree.csv + } + + if ($null -ne $job.ItemCounts) { + $folderData.ItemCounts = $job.ItemCounts + $folderData.ItemCounts | Export-Csv $PSScriptRoot\ItemCounts.csv + } + } + } + + $folderData.IpmSubtreeByMailbox = $folderData.IpmSubtree | Group-Object ContentMailbox + $folderData.IpmSubtree | ForEach-Object { $folderData.ParentEntryIdCounts[$_.ParentEntryId] += 1 } + $folderData.IpmSubtree | ForEach-Object { $folderData.EntryIdDictionary[$_.EntryId] = $_ } + $folderData.NonIpmSubtree | ForEach-Object { $folderData.NonIpmEntryIdDictionary[$_.EntryId] = $_ } + $folderData.ItemCounts | ForEach-Object { + if ($_.ItemCount -gt 0) { + $folder = $folderData.EntryIdDictionary[$_.EntryId.ToString()] + + if ($null -ne $folder) { + $folder.ItemCount = $_.ItemCount + } + } + } + } + + end { + Write-Host "Get-FolderData duration $((Get-Date) - $startTime)" + Write-Host " IPM_SUBTREE folder count: $($folderData.IpmSubtree.Count)" + Write-Host " NON_IPM_SUBTREE folder count: $($folderData.NonIpmSubtree.Count)" + + return $folderData + } +} + +. $PSScriptRoot\Get-IpmSubtree.ps1 +. $PSScriptRoot\Get-NonIpmSubtree.ps1 +. $PSScriptRoot\Get-ItemCount.ps1 diff --git a/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 index defd1e0d1c..1cd94a73a0 100644 --- a/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 @@ -1,13 +1,14 @@ function Get-IpmSubtree { [CmdletBinding()] param ( - [Parameter()] - [bool] - $startFresh = $true + [Parameter(Position = 0)] + [string] + $Server ) begin { - $startTime = Get-Date + $WarningPreference = "SilentlyContinue" + Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Server/powershell" -Authentication Kerberos) | Out-Null $progressCount = 0 $errors = 0 $ipmSubtree = @() @@ -15,8 +16,6 @@ $sw.Start() $progressParams = @{ Activity = "Retrieving IPM_SUBTREE folders" - Id = 2 - ParentId = 1 } } @@ -59,19 +58,10 @@ } end { - if ($errors -lt 1) { - if ($progressCount -gt 0) { - Write-Progress @progressParams -Status "Saving" - $ipmSubtree | Export-Csv $PSScriptRoot\IpmSubtree.csv - } - } else { - $ipmSubtree = @() - } - Write-Progress @progressParams -Completed - Write-Host "Get-IpmSubtree duration $((Get-Date) - $startTime) folder count $($ipmSubtree.Count)" - - return $ipmSubtree + return [PSCustomObject]@{ + IpmSubtree = $ipmSubtree + } } } diff --git a/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 b/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 index 1e75b46a01..5333f5ebd2 100644 --- a/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 @@ -5,51 +5,39 @@ #> [CmdletBinding()] param ( - [Parameter()] - [PSCustomObject] - $FolderData + [Parameter(Position = 0)] + [string] + $Server ) begin { - $startTime = Get-Date + $WarningPreference = "SilentlyContinue" + Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Server/powershell" -Authentication Kerberos) | Out-Null $progressCount = 0 $sw = New-Object System.Diagnostics.Stopwatch $sw.Start() $progressParams = @{ Activity = "Getting public folder statistics" - Id = 2 - ParentId = 1 } } process { - $highestItemCountFolder = $FolderData.IpmSubtree | Sort-Object ItemCount -Descending | Select-Object -First 1 - if ($highestItemCountFolder.ItemCount -lt 1) { - Get-PublicFolderStatistics -ResultSize Unlimited | ForEach-Object { - $progressCount++ - if ($sw.ElapsedMilliseconds -gt 1000) { - $sw.Restart() - Write-Progress @progressParams -Status $progressCount - } - - if ($_.ItemCount -gt 0) { - $folder = $FolderData.EntryIdDictionary[$_.EntryId.ToString()] - - if ($null -ne $folder) { - $folder.ItemCount = $_.ItemCount - } - } + $itemCounts = Get-PublicFolderStatistics -ResultSize Unlimited | ForEach-Object { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -Status $progressCount } + + Select-Object -InputObject $_ -Property EntryId, ItemCount } } end { - if ($progressCount -gt 0) { - Write-Progress @progressParams -Status "Saving" - $FolderData.IpmSubtree | Export-Csv $PSScriptRoot\IpmSubtree.csv - } - Write-Progress @progressParams -Completed - Write-Host "Get-ItemCount duration" ((Get-Date) - $startTime) + + return [PSCustomObject]@{ + ItemCounts = $itemCounts + } } } diff --git a/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 index 2e8104c0a2..39ff65161b 100644 --- a/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 @@ -1,13 +1,14 @@ function Get-NonIpmSubtree { [CmdletBinding()] param ( - [Parameter()] - [bool] - $startFresh = $true + [Parameter(Position = 0)] + [string] + $Server ) begin { - $startTime = Get-Date + $WarningPreference = "SilentlyContinue" + Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Server/powershell" -Authentication Kerberos) | Out-Null $progressCount = 0 $errors = 0 $nonIpmSubtree = @() @@ -15,57 +16,40 @@ $sw.Start() $progressParams = @{ Activity = "Retrieving NON_IPM_SUBTREE folders" - Id = 2 - ParentId = 1 } } process { - if (-not $startFresh -and (Test-Path $PSScriptRoot\NonIpmSubtree.csv)) { - Write-Progress @progressParams - $nonIpmSubtree = Import-Csv $PSScriptRoot\NonIpmSubtree.csv - } else { - $nonIpmSubtree = Get-PublicFolder \non_ipm_subtree -Recurse -ResultSize Unlimited | - Select-Object Identity, EntryId, DumpsterEntryId, MailEnabled | - ForEach-Object { - $progressCount++ - $currentFolder = $_.Identity.ToString() - try { - # Updating progress too often has a perf impact, so we only update every second. - if ($sw.ElapsedMilliseconds -gt 1000) { - $sw.Restart() - Write-Progress @progressParams -Status $progressCount - } + $nonIpmSubtree = Get-PublicFolder \non_ipm_subtree -Recurse -ResultSize Unlimited | + Select-Object Identity, EntryId, DumpsterEntryId, MailEnabled | + ForEach-Object { + $progressCount++ + $currentFolder = $_.Identity.ToString() + try { + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + Write-Progress @progressParams -Status $progressCount + } - [PSCustomObject]@{ - Identity = $_.Identity.ToString() - EntryId = $_.EntryId.ToString() - DumpsterEntryId = if ($_.DumpsterEntryId) { $_.DumpsterEntryId.ToString() } else { $null } - MailEnabled = $_.MailEnabled - } - } catch { - $errors++ - Write-Error -Message $currentFolder -Exception $_.Exception - break + [PSCustomObject]@{ + Identity = $_.Identity.ToString() + EntryId = $_.EntryId.ToString() + DumpsterEntryId = if ($_.DumpsterEntryId) { $_.DumpsterEntryId.ToString() } else { $null } + MailEnabled = $_.MailEnabled } + } catch { + $errors++ + Write-Error -Message $currentFolder -Exception $_.Exception + break } - } + } } end { - if ($errors -lt 1) { - if ($progressCount -gt 0) { - Write-Progress @progressParams -Status "Saving" - $nonIpmSubtree | Export-Csv $PSScriptRoot\NonIpmSubtree.csv - } - } else { - $nonIpmSubtree = @() - } - Write-Progress @progressParams -Completed - Write-Host "Get-NonIpmSubtree duration $((Get-Date) - $startTime) folder count $($nonIpmSubtree.Count)" - - return $nonIpmSubtree + return [PSCustomObject]@{ + NonIpmSubtree = $nonIpmSubtree + } } } diff --git a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 index 55963da2e0..7044a17929 100644 --- a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 +++ b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 @@ -13,9 +13,7 @@ param ( $CsvFile ) -. $PSScriptRoot\Get-IpmSubtree.ps1 -. $PSScriptRoot\Get-NonIpmSubtree.ps1 -. $PSScriptRoot\Get-ItemCount.ps1 +. $PSScriptRoot\Get-FolderData.ps1 . $PSScriptRoot\Get-LimitsExceeded.ps1 . $PSScriptRoot\Get-BadDumpsterMappings.ps1 . $PSScriptRoot\Get-BadPermission.ps1 @@ -38,40 +36,14 @@ $progressParams = @{ Id = 1 } -Write-Progress @progressParams -Status "Step 1 of 8" +Write-Progress @progressParams -Status "Step 1 of 5" -$ipmSubtree = Get-IpmSubtree -startFresh $StartFresh +$folderData = Get-FolderData -StartFresh $StartFresh -if ($ipmSubtree.Count -lt 1) { +if ($folderData.IpmSubtree.Count -lt 1) { return } -Write-Progress @progressParams -Status "Step 2 of 8" - -$nonIpmSubtree = Get-NonIpmSubtree -startFresh $StartFresh - -Write-Progress @progressParams -Status "Step 3 of 8" - -$hashtableProgress = @{ - Activity = "Populating hashtables" - Id = 2 - ParentId = 1 -} -Write-Progress @hashtableProgress - -$folderData = [PSCustomObject]@{ - IpmSubtree = $ipmSubtree - IpmSubtreeByMailbox = $ipmSubtree | Group-Object ContentMailbox - ParentEntryIdCounts = @{} - EntryIdDictionary = @{} - NonIpmSubtree = $nonIpmSubtree - NonIpmEntryIdDictionary = @{} - MailboxToServerMap = @{} -} - -$ipmSubtree | ForEach-Object { $folderData.ParentEntryIdCounts[$_.ParentEntryId] += 1 } -$ipmSubtree | ForEach-Object { $folderData.EntryIdDictionary[$_.EntryId] = $_ } -$nonIpmSubtree | ForEach-Object { $folderData.NonIpmEntryIdDictionary[$_.EntryId] = $_ } $script:anyDatabaseDown = $false Get-Mailbox -PublicFolder | ForEach-Object { try { @@ -93,27 +65,21 @@ if ($script:anyDatabaseDown) { return } -Write-Progress @hashtableProgress -Completed - -Write-Progress @progressParams -Status "Step 4 of 8" - -Get-ItemCount -FolderData $FolderData - # Now we're ready to do the checks -Write-Progress @progressParams -Status "Step 5 of 8" +Write-Progress @progressParams -Status "Step 2 of 5" $badDumpsters = @(Get-BadDumpsterMappings -FolderData $folderData) -Write-Progress @progressParams -Status "Step 6 of 8" +Write-Progress @progressParams -Status "Step 3 of 5" $limitsExceeded = Get-LimitsExceeded -FolderData $folderData -Write-Progress @progressParams -Status "Step 7 of 8" +Write-Progress @progressParams -Status "Step 4 of 5" $badMailEnabled = Get-BadMailEnabledFolder -FolderData $folderData -Write-Progress @progressParams -Status "Step 8 of 8" +Write-Progress @progressParams -Status "Step 5 of 5" $badPermissions = @(Get-BadPermission -FolderData $folderData) From b5b88df741e13db1fe54a546f08ec43bef4f2fd2 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 20:07:46 -0700 Subject: [PATCH 6/9] Remove redundant statement --- .../src/SourceSideValidations/Remove-InvalidPermission.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 index d47b7d2a4c..89f98491e9 100644 --- a/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 +++ b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 @@ -28,8 +28,7 @@ function Remove-InvalidPermission { Write-Progress @progressParams -Status "$progressCount / $($badPermissions.Count)" -PercentComplete ($progressCount * 100 / $badPermissions.Count) -CurrentOperation $permission.Identity } - if (-not $entryIdsProcessed.Contains($permission.EntryId)) { - $entryIdsProcessed.Add($permission.EntryId) | Out-Null + if ($entryIdsProcessed.Add($permission.EntryId)) { $permsOnFolder = Get-PublicFolderClientPermission -Identity $permission.EntryId $permsOnFolder | ForEach-Object { if ( From 5722e0c3ceac915b2b9148366b89c74b34c5448b Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 20:12:39 -0700 Subject: [PATCH 7/9] Improve progress indicator --- .../src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 index e0c21aebf0..3d473ac368 100644 --- a/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 @@ -28,11 +28,14 @@ $mailEnabledFoldersWithNoADObject = @() $mailPublicFoldersLinked = New-Object 'System.Collections.Generic.Dictionary[string, object]' $progressParams.Activity = "Checking for missing AD objects" + $startTimeForThisCheck = Get-Date for ($i = 0; $i -lt $ipmSubtreeMailEnabled.Count; $i++) { $progressCount++ if ($sw.ElapsedMilliseconds -gt 1000) { $sw.Restart() - Write-Progress @progressParams -PercentComplete ($i * 100 / $ipmSubtreeMailEnabled.Count) -Status ("$i of $($ipmSubtreeMailEnabled.Count)") + $elapsed = ((Get-Date) - $startTimeForThisCheck) + $estimatedRemaining = [TimeSpan]::FromTicks($ipmSubtreeMailEnabled.Count / $progressCount * $elapsed.Ticks - $elapsed.Ticks).ToString("hh\:mm\:ss") + Write-Progress @progressParams -PercentComplete ($i * 100 / $ipmSubtreeMailEnabled.Count) -Status ("$i of $($ipmSubtreeMailEnabled.Count) Estimated time remaining: $estimatedRemaining") } $result = $ipmSubtreeMailEnabled[$i] | Get-MailPublicFolder -ErrorAction SilentlyContinue if ($null -eq $result) { From 2cabc04aad3d5cf5d359e83d6352886e7afde081 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 21:20:58 -0700 Subject: [PATCH 8/9] Adjust progress bar --- .../SourceSideValidations/Get-BadMailEnabledFolder.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 index 3d473ac368..dd0047b43e 100644 --- a/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 +++ b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 @@ -27,7 +27,7 @@ $mailEnabledFoldersWithNoADObject = @() $mailPublicFoldersLinked = New-Object 'System.Collections.Generic.Dictionary[string, object]' - $progressParams.Activity = "Checking for missing AD objects" + $progressParams.CurrentOperation = "Checking for missing AD objects" $startTimeForThisCheck = Get-Date for ($i = 0; $i -lt $ipmSubtreeMailEnabled.Count; $i++) { $progressCount++ @@ -49,7 +49,7 @@ } $progressCount = 0 - $progressParams.Activity = "Getting all MailPublicFolder objects" + $progressParams.CurrentOperation = "Getting all MailPublicFolder objects" $allMailPublicFolders = @(Get-MailPublicFolder -ResultSize Unlimited | ForEach-Object { $progressCount++ if ($sw.ElapsedMilliseconds -gt 1000) { @@ -60,7 +60,7 @@ $progressCount = 0 - $progressParams.Activity = "Checking for orphaned MailPublicFolders" + $progressParams.CurrentOperation = "Checking for orphaned MailPublicFolders" $orphanedMailPublicFolders = @($allMailPublicFolders | ForEach-Object { $progressCount++ if ($sw.ElapsedMilliseconds -gt 1000) { @@ -74,7 +74,7 @@ }) - $progressParams.Activity = "Building EntryId HashSets" + $progressParams.CurrentOperation = "Building EntryId HashSets" Write-Progress @progressParams $byEntryId = New-Object 'System.Collections.Generic.Dictionary[string, object]' $FolderData.IpmSubtree | ForEach-Object { $byEntryId.Add($_.EntryId.ToString(), $_) } @@ -86,7 +86,7 @@ $orphanedMPFsThatPointToAMailEnabledFolder = @() $orphanedMPFsThatPointToNothing = @() $emailAddressMergeCommands = @() - $progressParams.Activity = "Checking for orphans that point to a valid folder" + $progressParams.CurrentOperation = "Checking for orphans that point to a valid folder" for ($i = 0; $i -lt $orphanedMailPublicFolders.Count; $i++) { if ($sw.ElapsedMilliseconds -gt 1000) { $sw.Restart() From 102282a313b9f45873d7530c24ba365b3060ca43 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Mon, 12 Apr 2021 21:53:31 -0700 Subject: [PATCH 9/9] Add readme for Public Folder scripts --- PublicFolders/README.md | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 PublicFolders/README.md diff --git a/PublicFolders/README.md b/PublicFolders/README.md new file mode 100644 index 0000000000..9aad9504e2 --- /dev/null +++ b/PublicFolders/README.md @@ -0,0 +1,42 @@ +Script|More Info|Download +-|-|- +SourceSideValidations.ps1 | [More Info](https://github.com/microsoft/CSS-Exchange/tree/main/PublicFolders#sourcesidevalidationsps1) | [Download](https://github.com/microsoft/CSS-Exchange/releases/latest/download/SourceSideValidations.ps1) +ValidateMailEnabledPublicFolders.ps1 | [More Info](https://github.com/microsoft/CSS-Exchange/tree/main/PublicFolders#validatemailenabledpublicfoldersps1) | [Download](https://github.com/microsoft/CSS-Exchange/releases/latest/download/ValidateMailEnabledPublicFolders.ps1) + +# Public Folder scripts + +## [SourceSideValidations.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/SourceSideValidations.ps1) + +This script performs pre-migration public folder checks for Exchange 2013, 2016, and 2019. For Exchange 2010, please use previous script found [here](https://www.microsoft.com/en-us/download/details.aspx?id=100414). + +### Syntax + +Typically, the script should be run with no parameters: + +`.\SourceSideValidations.ps1` + +### Output + +The script will generate one more of the following files, and it will display +examples that show how to use them. Examine the script output for those details. + +File Name|Content|Use +-|-|- +IpmSubtree.csv|A subset of properties of all Public Folders|Running with -StartFresh $false loads this file instead of retrieving fresh data +ItemCounts.csv|EntryID and item count of every folder|Running with -StartFresh $false loads this file instead of retrieving fresh data +NonIpmSubtree.csv|A subset of properties of all System Folders|Running with -StartFresh $false loads this file instead of retrieving fresh data +FoldersToMailDisable.txt|Folders that should be mail-disabled, because they are system folders or because their mail objects are missing|Use with the command displayed in the script output to disable them +MailPublicFolderOrphans.txt|Mail objects that are not linked to any existing folder|Use with the command displayed in the script output to delete them +MailPublicFolderDuplicates.txt|Mail objects that point to folders which are linked to some other mail object|Use with the command displayed in the script output to delete them +AddAddressesFromDuplicates.ps1|Commands that add the email addresses from the folders listed in MailPublicFolderDuplicates.txt onto the mail objects currently linked to the folders|Run after deleting the duplicates to preserve the email addresses on the remaining valid mail object +MailDisabledWithProxyGuid.txt|Folders that are mail-disabled but have a mail object stamped on them|Pipe to Enable-MailPublicFolder using the syntax example shown in the script output to enable these +MailPublicFoldersDisconnected.txt|Mail objects that correspond to a valid, but mail-disabled, folder|These must be examined and corrected manually +BadDumpsterMappings.txt|Folders with invalid dumpster mappings|These folders can be deleted or the -ExcludeDumpsters switch can be used to skip the dumpsters during migration +TooManyChildFolders.txt|Folders that have too many child folders|Examine the list and manually reduce the number of child folders +PathTooDeep.txt|Folders that exceed the path depth limit|Examine the list and reduce the depth of these paths by moving or deleting folders +TooManyItems.txt|Folders that have too many items|Examine the list and manually reduce the number of items in these folders +InvalidPermissions.csv|Any invalid ACEs that were found|Use with -RemoveInvalidPermissions parameter to remove these + +## [ValidateMailEnabledPublicFolders.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/ValidateMailEnabledPublicFolders.ps1) + +This script performs pre-migration checks on mail-enabled folders on Exchange 2010 and up. Note that these checks are also included in the new SourceSideValidations.ps1 shown above for 2013 and up.