Get-PublicFolderIDFixReport.ps1
<#PSScriptInfo
.VERSION 2.2 .GUID 6cdcff5d-bf70-4297-aab7-a558335ec932 .AUTHOR Aaron Guilmette .COMPANYNAME Microsoft .COPYRIGHT 2021 .TAGS Public Folder idfix .LICENSEURI .PROJECTURI https://www.undocumented-features.com/2021/04/21/newly-revamped-get-publicfolderidfixreport-tool/ .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .DESCRIPTION Checks public folders for a number of conditions that could lead to problems during a public folder migration (either on-premises to on-premises or on-premises to cloud). .PRIVATEDATA #> <# .SYNOPSIS Public Folder IDFix and Error Check report. .NOTES 2021-04-30 - Resolved type in variable name. 2021-04-23 - Updated email address validation to stop using Net.Mail.MailAddress. Conditions checked: - Invalid primary SMTP address for mail-enabled public folders - Invalid characters in mail-enabled public folder aliases - Invalid characters in public folder names - Duplicate aliases - publicFolder Microsoft Exchange System Objects must reference a valid public folder - Orphaned publicFolder Microsoft Exchange System Objects #> [CmdletBinding()] param ( [string]$ExchangeServer, [string]$Logfile = (Get-Date -Format yyyy-MM-dd) + "_PublicFolderIDFixLog.txt", [string]$MailPublicFolderOutputFile = (Get-Date -Format yyyy-MM-dd) + "_MailPublicFolderIDFixReport.csv", [string]$PublicFolderOutputFile = (Get-Date -Format yyyy-MM-dd) + "_PublicFolderIDFixReport.csv", [string]$UnlinkedMESOOutputFile = (Get-Date -Format yyyy-MM-dd) + "_UnlinkedMESOOutputReport.csv", [switch]$MailEnabledPublicFoldersOnly ) ## Miscellaneous Functions # Verify AD Tools are installed for MESO object verification function VerifyADTools($ParamName) { Write-Log -LogFile $Logfile -LogLevel INFO -Message "Checking for Active Directory Module." # Check for Active Directory Module If (!(Get-Module -ListAvailable ActiveDirectory)) { Write-Log -LogFile $Logfile -LogLevel INFO -ConsoleOutput -Message "$($ParamName) requires the Active Directory Module. Attempting to install." Try { $Result = Add-WindowsFeature RSAT-ADDS-Tools switch ($Result.Success) { True { Write-Log -LogFile $Logfile -LogLevel SUCCESS -ConsoleOutput -Message "Feature Active Directory Domain Services Tools (RSAT-ADDS-Tools) successful." If ($Result.ExitCode -match "restart" -or $Result.RestartNeeded -match "Yes") { Write-Log -LogFile $Logfile -LogLevel WARN -ConsoleOutput -Message "A restart may be necessary to use the newly installed feature." } Import-Module ActiveDirectory } False { Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "Feature Active Directory Domain Services Tools (RSAT-ADDS-Tools unsuccessful." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Feature: $($Result.FeatureResult.DisplayName)" Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Result: $($Result.Success)" Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Exit code: $($Result.ExitCode)" } } } Catch { $ErrorMessage = $_ Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "An error has occurred during feature installation. Please see $($Logfile) for details." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Feature: $($Result.FeatureResult.DisplayName)" Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Result: $($Result.Success)" Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Exit code: $($Result.ExitCode)" } Finally { If ($DebugLogging) { Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Feature Display Name: $($Result.FeatureResult.DisplayName)" Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Feature Name: $($Result.FeatureResult.Name)" Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Result: $($Result.Success)" Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Restart Needed: $($Result.RestartNeeded)" Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Exit code: $($Result.ExitCode)" Write-Log -LogFile $Logfile -LogLevel DEBUG -Message "Skip reason: $($Result.FeatureResult.SkipReason)" } } } Else { Import-Module ActiveDirectory; Write-Log -LogFile $Logfile -LogLevel INFO -Message "Active Directory Module loaded." } If (!(Get-Module -ListAvailable ActiveDirectory)) { Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "Unable to install Active Directory module. $($ParamName) configuration will not be successful. Please re-run AADConnectPermissions.ps1 without DeviceWriteBack parameter to continue." Break } } # End Function VerifyADTools # Logging function function Write-Log([string[]]$Message, [string]$LogFile = $Script:LogFile, [switch]$ConsoleOutput, [ValidateSet("SUCCESS", "INFO", "WARN", "ERROR", "DEBUG")][string]$LogLevel) { $Message = $Message + $Input If (!$LogLevel) { $LogLevel = "INFO" } switch ($LogLevel) { SUCCESS { $Color = "Green" } INFO { $Color = "White" } WARN { $Color = "Yellow" } ERROR { $Color = "Red" } DEBUG { $Color = "Gray" } } if ($Message -ne $null -and $Message.Length -gt 0) { $TimeStamp = [System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss") if ($LogFile -ne $null -and $LogFile -ne [System.String]::Empty) { Out-File -Append -FilePath $LogFile -InputObject "[$TimeStamp] $Message" } if ($ConsoleOutput -eq $true) { Write-Host "[$TimeStamp] [$LogLevel] :: $Message" -ForegroundColor $Color } } } # End Function Write-Log function LocateExchange { If (!$ExchangeServer) { $global:ProgressPreference = "SilentlyContinue" Write-Progress -Activity "No Exchange server specified. Attempting to locate Exchange Servers registered in Configuration container." Write-Log -LogFile $Logfile -LogLevel WARN -Message "No Exchange server specified. Attempting to locate Exchange Servers registered in configuration container." [array]$ExchangeServers = (Get-ADObject -Filter { objectCategory -eq "msExchExchangeServer" } -SearchBase (Get-ADRootDSE).configurationNamingContext).Name If ($ExchangeServers) { $SuccessfulTest = @() Write-Log -LogFile $Logfile -LogLevel INFO -Message "Found $($ExchangeServers.Count) Exchange servers registered in configuration partition. Selecting a server." ForEach ($obj in $ExchangeServers) { $Result = Try { Test-NetConnection $obj -ea stop -wa silentlycontinue -Port 443 } catch { $Result = "FAIL" } If ($Result.TcpTestSucceeded -eq $True) { Write-Log -LogFile $Logfile -LogLevel SUCCESS -Message "Successfully connected to discovered Exchange Server: $($obj)." $SuccessfulTest += $obj } if ($Result.TcpTestSucceeded -eq $False) { Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Unable to connect to discovered Exchange Server: $($obj)." } } If ($SuccessfulTest) { $script:ExchangeServer = (Get-Random $SuccessfulTest) Write-Log -Logfile $Logfile -LogLevel SUCCESS -Message "Selected Exchange Server $($ExchangeServer)." Write-Progress -Activity "Selected Exchange Server $($ExchangeServer)." -Id 2 -Completed } Else { If (!$ExchangeServer) { Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Cannot locate or connect to an Exchange server. ExchangeServer parameter must be specified if CreateMailboxes parameter is used. Error Code: EXERR01" -ConsoleOutput Exit } } } $global:ProgressPreference = "Continue" } Else { Write-Log -LogFile $LogFile -LogLevel ERROR -Message "Cannot locate or connect to an Exchange Server. ExchangeServer parameter must be specified if CreateMailboxes parameter is used. Error Code: EXERR02" -ConsoleOutput Exit } # return $ExchangeServer } function ConnectToExchange { If (!($ExchangeServer)) { LocateExchange } # Connect to Exchange Server try { $SessionInfo = Get-PSSession if ($SessionInfo.ConfigurationName -match "Microsoft.Exchange" -and $SessionInfo.ComputerName -match $ExchangeServer) { Write-Log -Message "You are already connected to an Exchange instance." -LogLevel INFO -LogFile $Logfile } else { Write-Log -Message "Connecting to $($ExchangeServer)..." -LogFile $Logfile -LogLevel INFO $Session = New-PSSession -ConfigurationName Microsoft.Exchange -Authentication Kerberos -ConnectionUri http://$($ExchangeServer)/powershell -WarningAction SilentlyContinue -InformationAction SilentlyContinue Try { Import-PSSession $Session -WarningAction SilentlyContinue -DisableNameChecking -InformationAction SilentlyContinue -ea Stop | Out-Null } Catch { Write-Log "Cannot connect to Exchange Server $($ExchangeServer). EXERR200" -LogFile $Logfile -LogLevel ERROR -ConsoleOutput; Break } } } catch { Write-Log -Message "Cannot connect to Exchange Server $($ExchangeServer)." -LogFile $Logfile -LogLevel ERROR -ConsoleOutput; Break } } # End Function ConnectToExchange # Email Address validation function ValidateEmail($address) { $address -match "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$" } ## All public folder functions function Get-PublicFolderData { Try { Write-Progress -Activity "Gathering information on public folders." -Status "Please be patient, as this may take a while if you have a significant number of public folders." # Write-Host -NoNewLine -ForegroundColor Cyan "Gathering information on non-mail-enabled public folders. Please be patient, " # Write-Host -ForegroundColor Cyan "as this may take a while if you have a significant number of public folders." $Global:PublicFolders = Get-PublicFolder -Recurse -ResultSize Unlimited | select Name, Identity, EntryId } Catch { Write-Host -ForegroundColor Yellow "Failed to retrieve public folders." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Get-PublicFolderData: $($Error.Exception.Message)" } } ## Mail-enabled folder functions # Get Mail-Enabled Public Folder Data function Get-MEPFData { Try { Write-Progress -Activity "Gathering information on mail-enabled public folders." -Status "Please be patient, as this may take a while if you have a significant number of public folders." # Write-Host -NoNewLine -ForegroundColor Cyan "Gathering information on mail-enabled public folders. Please be patient, " # Write-Host -ForegroundColor Cyan "as this may take a while if you have a significant number of public folders." [array]$Global:MailPublicFolders = Get-MailPublicFolder -ResultSize Unlimited -EA SilentlyContinue -WA SilentlyContinue | Select Alias, DisplayName, DistinguishedName, EmailAddresses, EntryId, ExternalEmailAddress, Guid, Identity, LegacyExchangeDN, PrimarySmtpAddress, PublicFolderType, RecipientDisplayType, RecipientType, RecipientTypeDetails, WindowsEmailAddress Write-Progress -Activity "Gathering information on mail-enabled recipients." -Status "Please be patient, as this may take a while if you have a significant number of objects." [array]$Global:Recipients = Get-Recipient -resultsize Unlimited -ea silentlycontinue -wa SilentlyContinue | select Alias,Identity } Catch { Write-Host -ForegroundColor Yellow "Failed to retrieve mail-enabled public folders." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Get-MEPFData: $($Error.Exception.Message)" } } function Get-MESOData { Try { Write-Progress -Activity "Gathering information on Microsoft Exchange System Objects container." -CurrentOperation "Please be patient." # Write-Host -ForegroundColor Cyan "Gathering information on Microsoft Exchange System Objects container." $global:MESOObjects = Get-ADObject -Filter { objectClass -eq "PublicFolder" } -Properties mailNickName,displayName, msExchPublicFolderEntryId, mail, proxyAddresses } Catch { Write-Host -Fore Yellow "Error querying Microsoft Exchange System Objects container."; Break Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Get-MESOData: $($Error.Exception.Message)" } } # Connect to Directory VerifyADTools LocateExchange ConnectToExchange # Gather data Get-PublicFolderData Get-MEPFData Get-MESOData # Forward checks against mail public folders $MEPFObjectData = @() $i = 1 foreach ($folder in $MailPublicFolders) { Write-Progress -Activity "Processing Mail-enabled public folders" -Status "Processing $folder.Identity" -PercentComplete ($i++ / $MailPublicFolders.Count * 100) ### Check Alias <# Check to see if Alias has any bad or invalid characters in it, suggest new ones alias Rules for MailNickname per IDFix Guidelines - Must be unique - Invalid characters: {whitespace} \ ! # $ % & * + / = ? ^ ` { } | ~ < > ( ) ' ; : , [ ] " @ - may not begin or end with a period - less than 64 characters long #> If ($MEPFObject) { Remove-Variable MEPFObject } $MEPFObject = [pscustomobject]@{ MEPF = $Folder.DisplayName Path = $Folder.Identity InvalidChars = $null SuggestedAlias = $null AliasIsDuplicated = $null WindowsEmailAddressPresent = $null WindowsEmailAddressIsValid = $null WindowsEmailAddressInvalidData = $null PrimarySmtpAddressPresent = $null PrimarySmtpAddressIsValid = $null PrimarySmtpAddressInvalidData = $null PrimarySmtpAddressInEmailAddressesPresent = $null PrimarySmtpAddressInEmailAddressesValid = $null PrimarySmtpAddressInEmailAddressesInvalidData = $null MissingEntryIdInMESO = $null EntryId = $null } # Check to see if Alias has bad characters If ($Folder.Alias -match "^(\.)|[\\\!\#\$\%\&\*\+\/\=\?\^\`\{\}\|\~\<\>\(\)\'\;\:\,\[\]\""\@ ]|(\.)$|(\.\.)") { Write-Verbose "Folder $($Folder.DisplayName) alias ($($Folder.Alias)) has invalid characters." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Folder $($Folder.DisplayName) alias ($($folder.Alias)) has invalid characters." $InvalidChars = '[^a-zA-Z0-9]' $SuggestedAlias = $Folder.Alias -replace $InvalidChars, "" If ($SuggestedAlias -iin $Recipients.Alias) { Write-Verbose "$SuggestedAlias is in $Recipients.Alias" do { $SuggestedAlias = ($SuggestedAlias + "_" + ([guid]::NewGuid())).Replace("-", "") If ($SuggestedAlias.Length -gt 63) { $SuggestedAlias = $SuggestedAlias.Substring(0, 63) } } while ($SuggestedAlias -iin $Recipients.Alias) } $MEPFObject.InvalidChars = "True" $MEPFObject.SuggestedAlias = "$($SuggestedAlias)" If ($SuggestedAlias) { Remove-Variable SuggestedAlias } } # Check to see if Alias is duplicated else { Write-Verbose "Checking to see if alias ($($Folder.Alias)) is present more than once." $count = (($Recipients.Alias -match $Folder.Alias).Count) If ($count -gt 1) { Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Alias ($($Alias)) is in Recipients more than once." -ConsoleOutput do { $SuggestedAlias = ($Folder.Alias + "_" + ([guid]::NewGuid())).Replace("-", "") If ($SuggestedAlias.Length -gt 60) { $SuggestedAlias = $SuggestedAlias.Substring(0, 63) } } while ($SuggestedAlias -iin $Recipients.Alias) $MEPFObject.AliasIsDuplicated = "True" $MEPFObject.SuggestedAlias = "$($SuggestedAlias)" } If ($SuggestedAlias) { Remove-Variable SuggestedAlias } } #### <# Check SMTP addresses for validity. There are 2 core addresses: - LDAP mail attribute, shown in AD, reported as WindowsEmailAddress in Get-MailPublicFolder - LDAP proxyAddresses:SMTP value, reported as PrimarySmtpAddress in Get-MailPublicFolder #> # Construct Vars If ($WindowsEmailAddress) { Remove-Variable WindowsEmailAddress } If ($PrimarySmtpAddress) { Remove-Variable PrimarySmtpAddress } If ($PrimarySmtpAddressInEmailAddresses) { Remove-Variable PrimarySmtpAddressInEmailAddresses } try { $WindowsEmailAddress = $folder.WindowsEmailAddress } catch { } try { $PrimarySmtpAddress = $folder.PrimarySmtpAddress.ToString() } catch { } try { $PrimarySmtpAddressInEmailAddresses = ($folder.EmailAddresses | ? { $_ -clike "SMTP:*" }).SubString(5) } catch { } # Evaluate WindowsEmailAddress if ($WindowsEmailAddress -eq $null -or $WindowsEmailAddress -eq "") { $MEPFObject.WindowsEmailAddressPresent = "False" Write-Verbose "MEPF $($folder.Identity) does not have a valid WindowsEmailAddress value." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) does not have a valid WindowsEmailAddress value." } else { $MEPFObject.WindowsEmailAddressPresent = "True" if (ValidateEmail -address $WindowsEmailAddress) { # Do nothing, since the address is valid. } else { $MEPFObject.WindowsEmailAddressIsValid = "False" $MEPFObject.WindowsEmailAddressInvalidData = $WindowsEmailAddress.ToString() Write-Verbose "MEPF $($folder.Identity) has invalid WindowsEmailAddress value $($WindowsEmailAddress)." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) has invalid WindowsEmailAddress value $($WindowsEmailAddress)." } } # Evaluate PrimarySmtpAddress if ($PrimarySmtpAddress -eq $null -or $PrimarySmtpAddress -eq "") { $MEPFObject.PrimarySmtpAddressPresent = "False" Write-Verbose "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value." } else { $MEPFObject.PrimarySmtpAddressPresent = "True" If (ValidateEmail -address $PrimarySmtpAddress) { # Do nothing, since the address is valid. } Else { $MEPFObject.PrimarySmtpAddressIsValid = "False" $MEPFObject.PrimarySmtpAddressInvalidData = $PrimarySmtpAddress Write-Verbose "MEPF $($folder.Identity) has invalid PrimarySmtpAddress value $($PrimarySmtpAddress)." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) has invalid PrimarySmtpAddress value $($PrimarySmtpAddress)." } } # Evaluate PrimarySmtpAddressInEmailAddresses if ($PrimarySmtpAddressInEmailAddresses -eq $null -or $PrimarySmtpAddressInEmailAddresses -eq "" ) { $MEPFObject.PrimarySmtpAddressInEmailAddressesPresent = "False" Write-Verbose "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value in the EmailAddress array." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value in the EmailAddress array." } else { $MEPFObject.PrimarySmtpAddressInEmailAddressesPresent = "True" If (ValidateEmail -address $PrimarySmtpAddressInEmailAddresses) { # Do nothing, since the address is valid. } Else { $MEPFObject.PrimarySmtpAddressInEmailAddressesValid = "False" $MEPFObject.PrimarySmtpAddressInEmailAddressesInvalidData = $PrimarySmtpAddressInEmailAddresses Write-Verbose "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value in the EmailAddress array." Write-Log -LogFile $Logfile -LogLevel ERROR -Message "MEPF $($folder.Identity) does not have a valid PrimarySmtpAddress value in the EmailAddress array." } } #### # Check to see if MEPF has corresponding object in Microsoft Exchange System Objects container If ($Folder.EntryId -iin $MESOObjects.msExchPublicFolderEntryId) { # Do nothing. Entry ID from PublicFolder is present in MESO. } Else { <# Add MEPF that is missing a corresponding object in the MESO container. To resolve: 1. Capture the MEPF's SMTP and X500 proxy addresses 2. Mail-disable the folder. 3. Mail-enable the folder. 4. Re-add proxy addresses saved from step 1. #> $MEPFObject.MissingEntryIdInMESO = "True" $MEPFObject.EntryId = $Folder.EntryId } $MEPFObjectData += $MEPFObject } # Checks all public folders for invalid Name values (either "\" or "/" in Name property) $PublicFolderData = @() foreach ($Folder in $PublicFolders) { If ($Folder.Name -match "(\\|\/)") { Write-Verbose "Name value for $($Folder.Name) has invalid '\' or '/' characters." $InvalidChars = '[\/\\]' $SuggestedName = $Folder.Name -replace $InvalidChars, "" $InvalidPFData = [pscustomobject] @{ Name = $Folder.Name; SuggestedName = $SuggestedName Path = $Folder.Identity; } $PublicFolderData += $InvalidPFData Remove-Variable InvalidPFData, SuggestedName } } # Checks Microsoft Exchange System Object container for public folder objects whose # msExchPublicFolderEntryId value does not match the a folder in Get-MailPublicFolder. # This likely indicated orphaned objects in the MESO container. [array]$UnlinkedMESOReport = @() If ($MESOObjects -and $MailPublicFolders) { Write-Verbose "Looking for orphaned objects in the MESO container." foreach ($MEPF in $MESOObjects) { if ($MEPF.msExchPublicFolderEntryId -notin $MailPublicFolders.EntryId) { If ($MEPF.msExchPublicFolderEntryId -eq $null) { $EntryId = "NULL" } Else { $EntryId = $MEPF.msExchPublicFolderEntryId } $UnlinkedMESO = [pscustomobject]@{ "Name" = $MEPF.Name; "DN" = $MEPF.DistinguishedName; "mail" = $MEPF.Mail; "msExchPublicFolderEntryId" = $EntryId } $UnlinkedMESOReport += $UnlinkedMESO Write-Log -LogFile $Logfile -Message "Found potentially unlinked object ($($MEPF.Name)) in Microsoft Exchange System Objects container." -LogLevel INFO Remove-Variable UnlinkedMESO,EntryId } } } # Save Reports Write-Host -NoNewline "The file: "; Write-Host -NoNewLine -ForegroundColor Green "$($MailPublicFolderOutputFile) "; Write-Host "contains the IDFix data for mail-enabled public folders, including invalid alias and SMTP address information and mail-enabled public folders that have missing entries in the Microsoft Exchange System Objects container." $MEPFObjectData | Export-Csv $MailPublicFolderOutputFile -NoType -Force Write-Host "" Write-Host -NoNewline "The file: "; Write-Host -NoNewline -ForegroundColor Green "$($PublicFolderOutputFile) "; Write-Host "contains IDFix data for all public folders with invalid 'name' attribute data." $PublicFolderData | Export-Csv $PublicFolderOutputFile -NoType -Force Write-Host "" Write-Host -NoNewline "The file: "; Write-Host -NoNewline -ForegroundColor Green "$($UnlinkedMESOOutputFile) "; Write-Host "contains potentially orphaned publicFolder objects in the Microsoft Exchange System Objects container. These are objects with no corresponding public folder in the store." $UnlinkedMESOReport | Export-Csv $UnlinkedMESOOutputFile -NoType -Force |