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. diff --git a/PublicFolders/RemoveInvalidPermissions.ps1 b/PublicFolders/RemoveInvalidPermissions.ps1 deleted file mode 100644 index 22359e9626..0000000000 --- a/PublicFolders/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/SourceSideValidations/Get-ItemCount.ps1 b/PublicFolders/SourceSideValidations/Get-ItemCount.ps1 deleted file mode 100644 index 1e75b46a01..0000000000 --- a/PublicFolders/SourceSideValidations/Get-ItemCount.ps1 +++ /dev/null @@ -1,55 +0,0 @@ -function Get-ItemCount { - <# - .SYNOPSIS - Populates the ItemCount property on our PSCustomObjects. - #> - [CmdletBinding()] - param ( - [Parameter()] - [PSCustomObject] - $FolderData - ) - - begin { - $startTime = Get-Date - $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 - } - } - } - } - } - - 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) - } -} diff --git a/PublicFolders/SourceSideValidations/Get-NonIpmSubtree.ps1 b/PublicFolders/SourceSideValidations/Get-NonIpmSubtree.ps1 deleted file mode 100644 index 2c9bdde09e..0000000000 --- a/PublicFolders/SourceSideValidations/Get-NonIpmSubtree.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -function Get-NonIpmSubtree { - [CmdletBinding()] - param ( - [Parameter()] - [bool] - $startFresh = $true - ) - - begin { - $startTime = Get-Date - $progressCount = 0 - $errors = 0 - $nonIpmSubtree = @() - $sw = New-Object System.Diagnostics.Stopwatch - $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 | - ForEach-Object { - $progressCount++ - $currentFolder = $_.Identity.ToString() - try { - # Updating progress too often has a perf impact, so we only update every 100 folders. - 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 } - } - } 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 - } -} diff --git a/PublicFolders/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/SourceSideValidations/SourceSideValidations.ps1 deleted file mode 100644 index 53fa217118..0000000000 --- a/PublicFolders/SourceSideValidations/SourceSideValidations.ps1 +++ /dev/null @@ -1,178 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter()] - [bool] - $StartFresh = $true -) - -. $PSScriptRoot\Get-IpmSubtree.ps1 -. $PSScriptRoot\Get-NonIpmSubtree.ps1 -. $PSScriptRoot\Get-ItemCount.ps1 -. $PSScriptRoot\Get-LimitsExceeded.ps1 -. $PSScriptRoot\Get-BadDumpsterMappings.ps1 -. $PSScriptRoot\Get-BadPermission.ps1 -. $PSScriptRoot\Get-BadPermissionJob.ps1 -. $PSScriptRoot\JobQueue.ps1 - -$startTime = Get-Date - -Set-ADServerSettings -ViewEntireForest $true - -$progressParams = @{ - Activity = "Validating public folders" - Id = 1 -} - -Write-Progress @progressParams -Status "Step 1 of 7" - -$ipmSubtree = Get-IpmSubtree -startFresh $StartFresh - -if ($ipmSubtree.Count -lt 1) { - return -} - -Write-Progress @progressParams -Status "Step 2 of 7" - -$nonIpmSubtree = Get-NonIpmSubtree -startFresh $StartFresh - -Write-Progress @progressParams -Status "Step 3 of 7" - -$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 { - $db = Get-MailboxDatabase $_.Database -Status - if ($db.Mounted) { - $folderData.MailboxToServerMap[$_.DisplayName] = $db.Server - } else { - Write-Error "Database $db is not mounted. This database holds PF mailbox $_ and must be mounted." - $script:anyDatabaseDown = $true - } - } catch { - Write-Error $_ - $script:anyDatabaseDown = $true - } -} - -if ($script:anyDatabaseDown) { - Write-Host "One or more PF mailboxes cannot be reached. Unable to proceed." - return -} - -Write-Progress @hashtableProgress -Completed - -Write-Progress @progressParams -Status "Step 4 of 7" - -Get-ItemCount -FolderData $FolderData - -# Now we're ready to do the checks - -Write-Progress @progressParams -Status "Step 5 of 7" - -$badDumpsters = @(Get-BadDumpsterMappings -FolderData $folderData) - -Write-Progress @progressParams -Status "Step 6 of 7" - -$limitsExceeded = Get-LimitsExceeded -FolderData $folderData - -Write-Progress @progressParams -Status "Step 7 of 7" - -$badPermissions = @(Get-BadPermission -FolderData $folderData) - -# Output the results - -if ($badDumpsters.Count -gt 0) { - $badDumpsterFile = Join-Path $PSScriptRoot "BadDumpsterMappings.txt" - Set-Content -Path $badDumpsterFile -Value $badDumpsters - - Write-Host - Write-Host $badDumpsters.Count "folders have invalid dumpster mappings. These folders are listed in" - Write-Host "the following file:" - Write-Host $badDumpsterFile -ForegroundColor Green - Write-Host "The -ExcludeDumpsters switch can be used to skip these folders during migration, or the" - Write-Host "folders can be deleted." -} - -if ($limitsExceeded.ChildCount.Count -gt 0) { - $tooManyChildFoldersFile = Join-Path $PSScriptRoot "TooManyChildFolders.txt" - Set-Content -Path $tooManyChildFoldersFile -Value $limitsExceeded.ChildCount - - Write-Host - Write-Host $limitsExceeded.ChildCount.Count "folders have exceeded the child folder limit of 10,000. These folders are" - Write-Host "listed in the following file:" - Write-Host $tooManyChildFoldersFile -ForegroundColor Green - Write-Host "Under each of the listed folders, child folders should be relocated or deleted to reduce this number." -} - -if ($limitsExceeded.FolderPathDepth.Count -gt 0) { - $pathTooDeepFile = Join-Path $PSScriptRoot "PathTooDeep.txt" - Set-Content -Path $pathTooDeepFile -Value $limitsExceeded.FolderPathDepth - - Write-Host - Write-Host $limitsExceeded.FolderPathDepth.Count "folders have exceeded the path depth limit of 299. These folders are" - Write-Host "listed in the following file:" - Write-Host $pathTooDeepFile -ForegroundColor Green - Write-Host "These folders should be relocated to reduce the path depth, or deleted." -} - -if ($limitsExceeded.ItemCount.Count -gt 0) { - $tooManyItemsFile = Join-Path $PSScriptRoot "TooManyItems.txt" - Set-Content -Path $tooManyItemsFile -Value $limitsExceeded.ItemCount - - Write-Host - Write-Host $limitsExceeded.ItemCount.Count "folders exceed the maximum of 1 million items. These folders are listed" - Write-Host "in the following file:" - Write-Host $tooManyItemsFile - Write-Host "In each of these folders, items should be deleted to reduce the item count." -} - -if ($badPermissions.Count -gt 0) { - $badPermissionsFile = Join-Path $PSScriptRoot "InvalidPermissions.csv" - $badPermissions | Export-Csv -Path $badPermissionsFile -NoTypeInformation - - 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" -} - -$folderCountMigrationLimit = 250000 - -if ($folderData.IpmSubtree.Count -gt $folderCountMigrationLimit) { - Write-Host - Write-Host "There are $($folderData.IpmSubtree.Count) public folders in the hierarchy. This exceeds" - Write-Host "the supported migration limit of $folderCountMigrationLimit for Exchange Online. The number" - Write-Host "of public folders must be reduced prior to migrating to Exchange Online." -} elseif ($folderData.IpmSubtree.Count * 2 -gt $folderCountMigrationLimit) { - Write-Host - Write-Host "There are $($folderData.IpmSubtree.Count) public folders in the hierarchy. Because each of these" - Write-Host "has a dumpster folder, the total number of folders to migrate will be $($folderData.IpmSubtree.Count * 2)." - Write-Host "This exceeds the supported migration limit of $folderCountMigrationLimit for Exchange Online." - Write-Host "New-MigrationBatch can be run with the -ExcludeDumpsters switch to skip the dumpster" - Write-Host "folders, or public folders may be deleted to reduce the number of folders." -} - -$private:endTime = Get-Date - -Write-Host -Write-Host "SourceSideValidations complete. Total duration" ($endTime - $startTime) 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/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 new file mode 100644 index 0000000000..dd0047b43e --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Get-BadMailEnabledFolder.ps1 @@ -0,0 +1,175 @@ +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.CurrentOperation = "Checking for missing AD objects" + $startTimeForThisCheck = Get-Date + for ($i = 0; $i -lt $ipmSubtreeMailEnabled.Count; $i++) { + $progressCount++ + if ($sw.ElapsedMilliseconds -gt 1000) { + $sw.Restart() + $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) { + $mailEnabledFoldersWithNoADObject += $ipmSubtreeMailEnabled[$i] + } else { + $guidString = $result.Guid.ToString() + if (-not $mailPublicFoldersLinked.ContainsKey($guidString)) { + $mailPublicFoldersLinked.Add($guidString, $result) | Out-Null + } + } + } + + $progressCount = 0 + $progressParams.CurrentOperation = "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.CurrentOperation = "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.CurrentOperation = "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.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() + 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/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/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/SourceSideValidations/Get-IpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 similarity index 55% rename from PublicFolders/SourceSideValidations/Get-IpmSubtree.ps1 rename to PublicFolders/src/SourceSideValidations/Get-IpmSubtree.ps1 index a330999f2a..1cd94a73a0 100644 --- a/PublicFolders/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 } } @@ -26,7 +25,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 +36,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++ @@ -57,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 new file mode 100644 index 0000000000..5333f5ebd2 --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Get-ItemCount.ps1 @@ -0,0 +1,43 @@ +function Get-ItemCount { + <# + .SYNOPSIS + Populates the ItemCount property on our PSCustomObjects. + #> + [CmdletBinding()] + param ( + [Parameter(Position = 0)] + [string] + $Server + ) + + begin { + $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" + } + } + + process { + $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 { + Write-Progress @progressParams -Completed + + return [PSCustomObject]@{ + ItemCounts = $itemCounts + } + } +} 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/src/SourceSideValidations/Get-NonIpmSubtree.ps1 b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 new file mode 100644 index 0000000000..39ff65161b --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Get-NonIpmSubtree.ps1 @@ -0,0 +1,55 @@ +function Get-NonIpmSubtree { + [CmdletBinding()] + param ( + [Parameter(Position = 0)] + [string] + $Server + ) + + begin { + $WarningPreference = "SilentlyContinue" + Import-PSSession (New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$Server/powershell" -Authentication Kerberos) | Out-Null + $progressCount = 0 + $errors = 0 + $nonIpmSubtree = @() + $sw = New-Object System.Diagnostics.Stopwatch + $sw.Start() + $progressParams = @{ + Activity = "Retrieving NON_IPM_SUBTREE folders" + } + } + + process { + $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 + } + } + } + + end { + Write-Progress @progressParams -Completed + + return [PSCustomObject]@{ + NonIpmSubtree = $nonIpmSubtree + } + } +} 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/src/SourceSideValidations/Remove-InvalidPermission.ps1 b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 new file mode 100644 index 0000000000..89f98491e9 --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/Remove-InvalidPermission.ps1 @@ -0,0 +1,51 @@ +function Remove-InvalidPermission { + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter()] + [string] + $CsvFile + ) + + 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 ($entryIdsProcessed.Add($permission.EntryId)) { + $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 diff --git a/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 new file mode 100644 index 0000000000..7044a17929 --- /dev/null +++ b/PublicFolders/src/SourceSideValidations/SourceSideValidations.ps1 @@ -0,0 +1,234 @@ +[CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess)] +param ( + [Parameter(Mandatory = $false, ParameterSetName = "Default")] + [bool] + $StartFresh = $true, + + [Parameter(Mandatory = $true, ParameterSetName = "RemoveInvalidPermissions")] + [Switch] + $RemoveInvalidPermissions, + + [Parameter(Mandatory = $true, ParameterSetName = "RemoveInvalidPermissions")] + [string] + $CsvFile +) + +. $PSScriptRoot\Get-FolderData.ps1 +. $PSScriptRoot\Get-LimitsExceeded.ps1 +. $PSScriptRoot\Get-BadDumpsterMappings.ps1 +. $PSScriptRoot\Get-BadPermission.ps1 +. $PSScriptRoot\Get-BadPermissionJob.ps1 +. $PSScriptRoot\JobQueue.ps1 +. $PSScriptRoot\Remove-InvalidPermission.ps1 +. $PSScriptRoot\Get-BadMailEnabledFolder.ps1 + +if ($RemoveInvalidPermissions) { + Remove-InvalidPermission -CsvFile $CsvFile + return +} + +$startTime = Get-Date + +Set-ADServerSettings -ViewEntireForest $true + +$progressParams = @{ + Activity = "Validating public folders" + Id = 1 +} + +Write-Progress @progressParams -Status "Step 1 of 5" + +$folderData = Get-FolderData -StartFresh $StartFresh + +if ($folderData.IpmSubtree.Count -lt 1) { + return +} + +$script:anyDatabaseDown = $false +Get-Mailbox -PublicFolder | ForEach-Object { + try { + $db = Get-MailboxDatabase $_.Database -Status + if ($db.Mounted) { + $folderData.MailboxToServerMap[$_.DisplayName] = $db.Server + } else { + Write-Error "Database $db is not mounted. This database holds PF mailbox $_ and must be mounted." + $script:anyDatabaseDown = $true + } + } catch { + Write-Error $_ + $script:anyDatabaseDown = $true + } +} + +if ($script:anyDatabaseDown) { + Write-Host "One or more PF mailboxes cannot be reached. Unable to proceed." + return +} + +# Now we're ready to do the checks + +Write-Progress @progressParams -Status "Step 2 of 5" + +$badDumpsters = @(Get-BadDumpsterMappings -FolderData $folderData) + +Write-Progress @progressParams -Status "Step 3 of 5" + +$limitsExceeded = Get-LimitsExceeded -FolderData $folderData + +Write-Progress @progressParams -Status "Step 4 of 5" + +$badMailEnabled = Get-BadMailEnabledFolder -FolderData $folderData + +Write-Progress @progressParams -Status "Step 5 of 5" + +$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 + + Write-Host + Write-Host $badDumpsters.Count "folders have invalid dumpster mappings. These folders are listed in" + Write-Host "the following file:" + Write-Host $badDumpsterFile -ForegroundColor Green + Write-Host "The -ExcludeDumpsters switch can be used to skip these folders during migration, or the" + Write-Host "folders can be deleted." +} + +if ($limitsExceeded.ChildCount.Count -gt 0) { + $tooManyChildFoldersFile = Join-Path $PSScriptRoot "TooManyChildFolders.txt" + Set-Content -Path $tooManyChildFoldersFile -Value $limitsExceeded.ChildCount + + Write-Host + Write-Host $limitsExceeded.ChildCount.Count "folders have exceeded the child folder limit of 10,000. These folders are" + Write-Host "listed in the following file:" + Write-Host $tooManyChildFoldersFile -ForegroundColor Green + Write-Host "Under each of the listed folders, child folders should be relocated or deleted to reduce this number." +} + +if ($limitsExceeded.FolderPathDepth.Count -gt 0) { + $pathTooDeepFile = Join-Path $PSScriptRoot "PathTooDeep.txt" + Set-Content -Path $pathTooDeepFile -Value $limitsExceeded.FolderPathDepth + + Write-Host + Write-Host $limitsExceeded.FolderPathDepth.Count "folders have exceeded the path depth limit of 299. These folders are" + Write-Host "listed in the following file:" + Write-Host $pathTooDeepFile -ForegroundColor Green + Write-Host "These folders should be relocated to reduce the path depth, or deleted." +} + +if ($limitsExceeded.ItemCount.Count -gt 0) { + $tooManyItemsFile = Join-Path $PSScriptRoot "TooManyItems.txt" + Set-Content -Path $tooManyItemsFile -Value $limitsExceeded.ItemCount + + Write-Host + Write-Host $limitsExceeded.ItemCount.Count "folders exceed the maximum of 1 million items. These folders are listed" + Write-Host "in the following file:" + Write-Host $tooManyItemsFile + Write-Host "In each of these folders, items should be deleted to reduce the item count." +} + +if ($badPermissions.Count -gt 0) { + $badPermissionsFile = Join-Path $PSScriptRoot "InvalidPermissions.csv" + $badPermissions | Export-Csv -Path $badPermissionsFile -NoTypeInformation + + 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 switch as follows:" + Write-Host ".\SourceSideValidations.ps1 -RemoveInvalidPermissions -CsvFile $badPermissionsFile" +} + +$folderCountMigrationLimit = 250000 + +if ($folderData.IpmSubtree.Count -gt $folderCountMigrationLimit) { + Write-Host + Write-Host "There are $($folderData.IpmSubtree.Count) public folders in the hierarchy. This exceeds" + Write-Host "the supported migration limit of $folderCountMigrationLimit for Exchange Online. The number" + Write-Host "of public folders must be reduced prior to migrating to Exchange Online." +} elseif ($folderData.IpmSubtree.Count * 2 -gt $folderCountMigrationLimit) { + Write-Host + Write-Host "There are $($folderData.IpmSubtree.Count) public folders in the hierarchy. Because each of these" + Write-Host "has a dumpster folder, the total number of folders to migrate will be $($folderData.IpmSubtree.Count * 2)." + Write-Host "This exceeds the supported migration limit of $folderCountMigrationLimit for Exchange Online." + Write-Host "New-MigrationBatch can be run with the -ExcludeDumpsters switch to skip the dumpster" + Write-Host "folders, or public folders may be deleted to reduce the number of folders." +} + +$private:endTime = Get-Date + +Write-Host +Write-Host "SourceSideValidations complete. Total duration" ($endTime - $startTime) 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