Get-PublicFolderIDFixReport.ps1

<#PSScriptInfo
 
.VERSION 2.3
 
.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.
           - Updated to check MEPFs for null alias.
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 is null
    If ($Folder.Alias -eq $null)
    {
        Write-Verbose "Folder $($Folder.DisplayName) alias ($($Folder.Alias)) is null."
        Write-Log -LogFile $Logfile -LogLevel ERROR -Message "Folder $($Folder.DisplayName) alias ($($folder.Alias)) is null."
        $InvalidChars = '[^a-zA-Z0-9\s]'
        $SuggestedAlias = $Folder.DisplayName -replace $InvalidChars, ""
        $SuggestedAlias = $SuggestedAlias.Substring(15)
        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 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)) for folder ($($folder.DisplayName)) 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)) of folder ($($folder.DisplayName)) 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