ExchangeEssentials.psm1

function Convert-ExchangeEmail { 
    <#
    .SYNOPSIS
    Function that helps converting Exchange email address list into readable, exportable format.
     
    .DESCRIPTION
        Function that helps converting Exchange email address list into readable, exportable format.
     
    .PARAMETER Emails
    List of emails as available in Exchange or Exchange Online, otherwise known as proxy addresses list
     
    .PARAMETER Separator
     
    .PARAMETER RemoveDuplicates
     
    .PARAMETER RemovePrefix
     
    .PARAMETER AddSeparator
     
    .EXAMPLE
     
    $Emails = @()
    $Emails += 'SIP:test@email.com'
    $Emails += 'SMTP:elo@maiu.com'
    $Emails += 'sip:elo@maiu.com'
    $Emails += 'Spo:dfte@sdsd.com'
    $Emails += 'SPO:myothertest@sco.com'
 
    Convert-ExchangeEmail -Emails $Emails -RemovePrefix -RemoveDuplicates -AddSeparator
     
    .NOTES
    General notes
    #>

    
    [CmdletBinding()]
    param(
        [string[]] $Emails,
        [string] $Separator = ', ',
        [switch] $RemoveDuplicates,
        [switch] $RemovePrefix,
        [switch] $AddSeparator
    )

    if ($RemovePrefix) {
        #$Emails = $Emails.Replace('SMTP:', '').Replace('SIP:', '').Replace('smtp:', '').Replace('sip:', '').Replace('spo:','')
        $Emails = $Emails -replace 'smtp:', '' -replace 'sip:', '' -replace 'spo:', ''
    }
    if ($RemoveDuplicates) {
        $Emails = $Emails | Sort-Object -Unique
    }
    if ($AddSeparator) {
        $Emails = $Emails -join $Separator
    }
    return $Emails
}
function Convert-ExchangeSize { 
    [cmdletbinding()]
    param(
        [validateset("Bytes", "KB", "MB", "GB", "TB")][string]$To = 'MB',
        [string]$Size,
        [int]$Precision = 4,
        [switch]$Display,
        [string]$Default = 'N/A'
    )
    if ([string]::IsNullOrWhiteSpace($Size)) {
        return $Default
    }
    $Pattern = [Regex]::new('(?<=\()([0-9]*[,.].*[0-9])')  # (?<=\()([0-9]*.*[0-9]) works too
    $Value = ($Size | Select-String $Pattern -AllMatches).Matches.Value
    #Write-Verbose "Convert-ExchangeSize - Value Before: $Value"

    if ($null -ne $Value) {
        $Value = $Value.Replace(',', '').Replace('.', '')
    }

    switch ($To) {
        "Bytes" {
            return $value 
        }
        "KB" {
            $Value = $Value / 1KB 
        }
        "MB" {
            $Value = $Value / 1MB 
        }
        "GB" {
            $Value = $Value / 1GB 
        }
        "TB" {
            $Value = $Value / 1TB 
        }
    }
    #Write-Verbose "Convert-ExchangeSize - Value After: $Value"
    if ($Display) {
        return "$([Math]::Round($value,$Precision,[MidPointRounding]::AwayFromZero)) $To"
    }
    else {
        return [Math]::Round($value, $Precision, [MidPointRounding]::AwayFromZero)
    }
}
function ConvertFrom-DistinguishedName { 
    <#
    .SYNOPSIS
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .DESCRIPTION
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .PARAMETER DistinguishedName
    Distinguished Name to convert
 
    .PARAMETER ToOrganizationalUnit
    Converts DistinguishedName to Organizational Unit
 
    .PARAMETER ToDC
    Converts DistinguishedName to DC
 
    .PARAMETER ToDomainCN
    Converts DistinguishedName to Domain Canonical Name (CN)
 
    .PARAMETER ToCanonicalName
    Converts DistinguishedName to Canonical Name
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName
 
    Output:
    Przemyslaw Klys
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit
 
    Output:
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $Con = @(
        'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz'
        'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz'
        'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz'
        'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl'
        'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz'
    )
 
    ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName
 
    Output:
    Windows Authorization Access Group
    Mmm
    e6d5fd00-385d-4e65-b02d-9da3493ed850
    Domain Controllers
    Microsoft Exchange Security Groups
 
    .EXAMPLEE
    ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
 
    Output:
    ad.evotec.xyz
    ad.evotec.xyz\Production\Users
    ad.evotec.xyz\Production\Users\test
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToDC')]
        [Parameter(ParameterSetName = 'ToDomainCN')]
        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'ToLastName')]
        [Parameter(ParameterSetName = 'ToCanonicalName')]
        [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName,
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent,
        [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC,
        [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN,
        [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName,
        [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName
    )
    Process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                if ($CN) {
                    $CN
                }
            }
            elseif ($ToOrganizationalUnit) {
                $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value
                if ($Value) {
                    $Value
                }
            }
            elseif ($ToMultipleOrganizationalUnit) {
                if ($IncludeParent) {
                    $Distinguished
                }
                while ($true) {
                    #$dn = $dn -replace '^.+?,(?=CN|OU|DC)'
                    $Distinguished = $Distinguished -replace '^.+?,(?=..=)'
                    if ($Distinguished -match '^DC=') {
                        break
                    }
                    $Distinguished
                }
            }
            elseif ($ToDC) {
                #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value
                # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value
                $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                if ($Value) {
                    $Value
                }
                #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value
            }
            elseif ($ToLastName) {
                # Would be best if it worked, but there is too many edge cases so hand splits seems to be the best solution
                # Feel free to change it back to regex if you know how ;)
                <# https://stackoverflow.com/questions/51761894/regex-extract-ou-from-distinguished-name
                $Regex = "^(?:(?<cn>CN=(?<name>.*?)),)?(?<parent>(?:(?<path>(?:CN|OU).*?),)?(?<domain>(?:DC=.*)+))$"
                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.name
                }
                #>

                $NewDN = $Distinguished -split ",DC="
                if ($NewDN[0].Contains(",OU=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",OU="
                }
                elseif ($NewDN[0].Contains(",CN=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",CN="
                }
                else {
                    [Array] $ChangedDN = $NewDN[0]
                }
                if ($ChangedDN[0].StartsWith('CN=')) {
                    $ChangedDN[0] -replace 'CN=', ''
                }
                else {
                    $ChangedDN[0] -replace 'OU=', ''
                }
            }
            elseif ($ToCanonicalName) {
                $Domain = $null
                $Rest = $null
                foreach ($O in $Distinguished -split '(?<!\\),') {
                    if ($O -match '^DC=') {
                        $Domain += $O.Substring(3) + '.'
                    }
                    else {
                        $Rest = $O.Substring(3) + '\' + $Rest
                    }
                }
                if ($Domain -and $Rest) {
                    $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',')
                }
                elseif ($Domain) {
                    $Domain.Trim('.')
                }
                elseif ($Rest) {
                    $Rest.TrimEnd('\') -replace '\\,', ','
                }
            }
            else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'
                #$Output = foreach ($_ in $Distinguished) {
                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.cn
                }
                #}
                #$Output.cn
            }
        }
    }
}
function Copy-Dictionary { 
    [alias('Copy-Hashtable', 'Copy-OrderedHashtable')]
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )
    # create a deep-clone of an object
    $ms = [System.IO.MemoryStream]::new()
    $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new()
    $bf.Serialize($ms, $Dictionary)
    $ms.Position = 0
    $clone = $bf.Deserialize($ms)
    $ms.Close()
    $clone
}
function Get-FileName { 
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Extension
    Parameter description
 
    .PARAMETER Temporary
    Parameter description
 
    .PARAMETER TemporaryFileOnly
    Parameter description
 
    .EXAMPLE
    Get-FileName -Temporary
    Output: 3ymsxvav.tmp
 
    .EXAMPLE
 
    Get-FileName -Temporary
    Output: C:\Users\pklys\AppData\Local\Temp\tmpD74C.tmp
 
    .EXAMPLE
 
    Get-FileName -Temporary -Extension 'xlsx'
    Output: C:\Users\pklys\AppData\Local\Temp\tmp45B6.xlsx
 
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $Extension = 'tmp',
        [switch] $Temporary,
        [switch] $TemporaryFileOnly
    )

    if ($Temporary) {
        # C:\Users\przemyslaw.klys\AppData\Local\Temp\p0v4bbif.xlsx
        return [io.path]::Combine([System.IO.Path]::GetTempPath(), "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension")
    }
    if ($TemporaryFileOnly) {
        # Generates 3ymsxvav.tmp
        return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension"
    }
}
function Get-GitHubVersion { 
    <#
    .SYNOPSIS
    Get the latest version of a GitHub repository and compare with local version
 
    .DESCRIPTION
    Get the latest version of a GitHub repository and compare with local version
 
    .PARAMETER Cmdlet
    Cmdlet to find module for
 
    .PARAMETER RepositoryOwner
    Repository owner
 
    .PARAMETER RepositoryName
    Repository name
 
    .EXAMPLE
    Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Cmdlet,
        [Parameter(Mandatory)][string] $RepositoryOwner,
        [Parameter(Mandatory)][string] $RepositoryName
    )
    $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue
    if ($App) {
        [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false)
        $LatestVersion = $GitHubReleases[0]
        if (-not $LatestVersion.Errors) {
            if ($App.Version -eq $LatestVersion.Version) {
                "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)"
            }
            elseif ($App.Version -lt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?"
            }
            elseif ($App.Version -gt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!"
            }
        }
        else {
            "Current: $($App.Version)"
        }
    }
}
function Get-WinADForestDetails { 
    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [string] $Filter = '*',
        [switch] $TestAvailability,
        [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All',
        [int[]] $Ports = 135,
        [int] $PortsTimeout = 100,
        [int] $PingCount = 1,
        [switch] $PreferWritable,
        [switch] $Extended,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    if ($Global:ProgressPreference -ne 'SilentlyContinue') {
        $TemporaryProgress = $Global:ProgressPreference
        $Global:ProgressPreference = 'SilentlyContinue'
    }

    if (-not $ExtendedForestInformation) {
        # standard situation, building data from AD
        $Findings = [ordered] @{ }
        try {
            if ($Forest) {
                $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest
            }
            else {
                $ForestInformation = Get-ADForest -ErrorAction Stop
            }
            <#
            $ForestInformation = [ordered] @{
                ApplicationPartitions = $ForestInf.ApplicationPartitions | ForEach-Object -Process { $_ } # : {DC=DomainDnsZones,DC=ad,DC=evotec,DC=xyz, DC=DomainDnsZones,DC=ad,DC=evotec,DC=pl, DC=ForestDnsZones,DC=ad,DC=evotec,DC=xyz}
                CrossForestReferences = $ForestInf.CrossForestReferences | ForEach-Object -Process { $_ } # : {}
                DomainNamingMaster = $ForestInf.DomainNamingMaster # : AD1.ad.evotec.xyz
                Domains = $ForestInf.Domains | ForEach-Object -Process { $_ } # : {ad.evotec.xyz, ad.evotec.pl}
                ForestMode = $ForestInf.ForestMode # : Windows2012R2Forest
                GlobalCatalogs = $ForestInf.GlobalCatalogs | ForEach-Object -Process { $_ } # : {AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, ADRODC.ad.evotec.pl, AD3.ad.evotec.xyz...}
                Name = $ForestInf.Name # : ad.evotec.xyz
                PartitionsContainer = $ForestInf.PartitionsContainer # : CN=Partitions,CN=Configuration,DC=ad,DC=evotec,DC=xyz
                RootDomain = $ForestInf.RootDomain # : ad.evotec.xyz
                SchemaMaster = $ForestInf.SchemaMaster # : AD1.ad.evotec.xyz
                Sites = $ForestInf.Sites | ForEach-Object -Process { $_ } # : {KATOWICE-1, KATOWICE-2}
                SPNSuffixes = $ForestInf.SPNSuffixes | ForEach-Object -Process { $_ } # : {}
                UPNSuffixes = $ForestInf.UPNSuffixes | ForEach-Object -Process { $_ } # : {myneva.eu, single.evotec.xyz, newUPN@com, evotec.xyz...}
            }
            #>

        }
        catch {
            Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)"
            return
        }
        if (-not $ForestInformation) {
            return
        }
        $Findings['Forest'] = $ForestInformation
        $Findings['ForestDomainControllers'] = @()
        $Findings['QueryServers'] = @{ }
        $Findings['DomainDomainControllers'] = @{ }
        [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) {
            if ($IncludeDomains) {
                if ($Domain -in $IncludeDomains) {
                    $Domain.ToLower()
                }
                # We skip checking for exclusions
                continue
            }
            if ($Domain -notin $ExcludeDomains) {
                $Domain.ToLower()
            }
        }
        # We want to have QueryServers always available for all domains
        [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) {
            try {
                $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent

                $OrderedDC = [ordered] @{
                    Domain      = $DC.Domain
                    Forest      = $DC.Forest
                    HostName    = [Array] $DC.HostName
                    IPv4Address = $DC.IPv4Address
                    IPv6Address = $DC.IPv6Address
                    Name        = $DC.Name
                    Site        = $DC.Site
                }
            }
            catch {
                Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)"
                continue
            }
            if ($Domain -eq $Findings['Forest']['Name']) {
                $Findings['QueryServers']['Forest'] = $OrderedDC
            }
            $Findings['QueryServers']["$Domain"] = $OrderedDC
            # lets return domain as something that wroks
            $Domain
        }

        # we need to make sure to remove domains that don't have DCs for some reason
        [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) {
            if ($Domain -notin $DomainsActive) {
                Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping."
                continue
            }
            $Domain
        }

        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0]

            [Array] $AllDC = try {
                try {
                    $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop
                }
                catch {
                    Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)"
                    continue
                }
                foreach ($S in $DomainControllers) {
                    if ($IncludeDomainControllers.Count -gt 0) {
                        If (-not $IncludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -notin $IncludeDomainControllers) {
                                continue
                            }
                        }
                        else {
                            if ($S.HostName -notin $IncludeDomainControllers) {
                                continue
                            }
                        }
                    }
                    if ($ExcludeDomainControllers.Count -gt 0) {
                        If (-not $ExcludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -in $ExcludeDomainControllers) {
                                continue
                            }
                        }
                        else {
                            if ($S.HostName -in $ExcludeDomainControllers) {
                                continue
                            }
                        }
                    }
                    $Server = [ordered] @{
                        Domain                 = $Domain
                        HostName               = $S.HostName
                        Name                   = $S.Name
                        Forest                 = $ForestInformation.RootDomain
                        Site                   = $S.Site
                        IPV4Address            = $S.IPV4Address
                        IPV6Address            = $S.IPV6Address
                        IsGlobalCatalog        = $S.IsGlobalCatalog
                        IsReadOnly             = $S.IsReadOnly
                        IsSchemaMaster         = ($S.OperationMasterRoles -contains 'SchemaMaster')
                        IsDomainNamingMaster   = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                        IsPDC                  = ($S.OperationMasterRoles -contains 'PDCEmulator')
                        IsRIDMaster            = ($S.OperationMasterRoles -contains 'RIDMaster')
                        IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                        OperatingSystem        = $S.OperatingSystem
                        OperatingSystemVersion = $S.OperatingSystemVersion
                        OperatingSystemLong    = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion
                        LdapPort               = $S.LdapPort
                        SslPort                = $S.SslPort
                        DistinguishedName      = $S.ComputerObjectDN
                        Pingable               = $null
                        WinRM                  = $null
                        PortOpen               = $null
                        Comment                = ''
                    }
                    if ($TestAvailability) {
                        if ($Test -eq 'All' -or $Test -like 'Ping*') {
                            $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount
                        }
                        if ($Test -eq 'All' -or $Test -like '*WinRM*') {
                            $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status
                        }
                        if ($Test -eq 'All' -or '*PortOpen*') {
                            $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status
                        }
                    }
                    [PSCustomObject] $Server
                }
            }
            catch {
                [PSCustomObject]@{
                    Domain                   = $Domain
                    HostName                 = ''
                    Name                     = ''
                    Forest                   = $ForestInformation.RootDomain
                    IPV4Address              = ''
                    IPV6Address              = ''
                    IsGlobalCatalog          = ''
                    IsReadOnly               = ''
                    Site                     = ''
                    SchemaMaster             = $false
                    DomainNamingMasterMaster = $false
                    PDCEmulator              = $false
                    RIDMaster                = $false
                    InfrastructureMaster     = $false
                    LdapPort                 = ''
                    SslPort                  = ''
                    DistinguishedName        = ''
                    Pingable                 = $null
                    WinRM                    = $null
                    PortOpen                 = $null
                    Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                }
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
                #$Findings[$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            }
            else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
                #$Findings[$Domain] = $AllDC
            }
            # Building all DCs for whole Forest
            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        if ($Extended) {
            $Findings['DomainsExtended'] = @{ }
            $Findings['DomainsExtendedNetBIOS'] = @{ }
            foreach ($DomainEx in $Findings['Domains']) {
                try {
                    #$Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0]

                    $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object {
                        # We need to use ForEach-Object to convert ADPropertyValueCollection to normal strings. Otherwise Copy-Dictionary fails
                        #True False ADPropertyValueCollection System.Collections.CollectionBase

                        [ordered] @{
                            AllowedDNSSuffixes                 = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ }                #: { }
                            ChildDomains                       = $_.ChildDomains | ForEach-Object -Process { $_ }                      #: { }
                            ComputersContainer                 = $_.ComputersContainer                 #: CN = Computers, DC = ad, DC = evotec, DC = xyz
                            DeletedObjectsContainer            = $_.DeletedObjectsContainer            #: CN = Deleted Objects, DC = ad, DC = evotec, DC = xyz
                            DistinguishedName                  = $_.DistinguishedName                  #: DC = ad, DC = evotec, DC = xyz
                            DNSRoot                            = $_.DNSRoot                            #: ad.evotec.xyz
                            DomainControllersContainer         = $_.DomainControllersContainer         #: OU = Domain Controllers, DC = ad, DC = evotec, DC = xyz
                            DomainMode                         = $_.DomainMode                         #: Windows2012R2Domain
                            DomainSID                          = $_.DomainSID.Value                        #: S - 1 - 5 - 21 - 853615985 - 2870445339 - 3163598659
                            ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer #: CN = ForeignSecurityPrincipals, DC = ad, DC = evotec, DC = xyz
                            Forest                             = $_.Forest                             #: ad.evotec.xyz
                            InfrastructureMaster               = $_.InfrastructureMaster               #: AD1.ad.evotec.xyz
                            LastLogonReplicationInterval       = $_.LastLogonReplicationInterval       #:
                            LinkedGroupPolicyObjects           = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ }           #:
                            LostAndFoundContainer              = $_.LostAndFoundContainer              #: CN = LostAndFound, DC = ad, DC = evotec, DC = xyz
                            ManagedBy                          = $_.ManagedBy                          #:
                            Name                               = $_.Name                               #: ad
                            NetBIOSName                        = $_.NetBIOSName                        #: EVOTEC
                            ObjectClass                        = $_.ObjectClass                        #: domainDNS
                            ObjectGUID                         = $_.ObjectGUID                         #: bc875580 - 4c70-41ad-a487-c57337e26024
                            ParentDomain                       = $_.ParentDomain                       #:
                            PDCEmulator                        = $_.PDCEmulator                        #: AD1.ad.evotec.xyz
                            PublicKeyRequiredPasswordRolling   = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ }   #:
                            QuotasContainer                    = $_.QuotasContainer                    #: CN = NTDS Quotas, DC = ad, DC = evotec, DC = xyz
                            ReadOnlyReplicaDirectoryServers    = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ }    #: { }
                            ReplicaDirectoryServers            = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ }           #: { AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, AD3.ad.evotec.xyz }
                            RIDMaster                          = $_.RIDMaster                          #: AD1.ad.evotec.xyz
                            SubordinateReferences              = $_.SubordinateReferences | ForEach-Object -Process { $_ }            #: { DC = ForestDnsZones, DC = ad, DC = evotec, DC = xyz, DC = DomainDnsZones, DC = ad, DC = evotec, DC = xyz, CN = Configuration, DC = ad, DC = evotec, DC = xyz }
                            SystemsContainer                   = $_.SystemsContainer                   #: CN = System, DC = ad, DC = evotec, DC = xyz
                            UsersContainer                     = $_.UsersContainer                     #: CN = Users, DC = ad, DC = evotec, DC = xyz
                        }
                    }

                    $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName']
                    $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx]
                }
                catch {
                    Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)"
                    continue
                }
            }
        }
        # Bring back setting as per default
        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }

        $Findings
    }
    else {
        # this takes care of limiting output to only what we requested, but based on prior input
        # this makes sure we ask once for all AD stuff and then subsequent calls just filter out things
        # this should be much faster then asking again and again for stuff from AD
        $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation
        [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) {
            if ($IncludeDomains) {
                if ($_ -in $IncludeDomains) {
                    $_.ToLower()
                }
                # We skip checking for exclusions
                continue
            }
            if ($_ -notin $ExcludeDomains) {
                $_.ToLower()
            }
        }
        # Now that we have Domains we need to remove all DCs that are not from domains we excluded or included
        foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainDomainControllers.Remove($_)
            }
        }
        # Same as above but for query servers - we don't remove queried servers
        #foreach ($_ in [string[]] $Findings.QueryServers.Keys) {
        # if ($_ -notin $Findings.Domains -and $_ -ne 'Forest') {
        # $Findings.QueryServers.Remove($_)
        # }
        #}
        # Now that we have Domains we need to remove all Domains that are excluded or included
        foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainsExtended.Remove($_)
                $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName'
                if ($NetBiosName) {
                    $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName)
                }
            }
        }
        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) {
                if ($IncludeDomainControllers.Count -gt 0) {
                    If (-not $IncludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -notin $IncludeDomainControllers) {
                            continue
                        }
                    }
                    else {
                        if ($S.HostName -notin $IncludeDomainControllers) {
                            continue
                        }
                    }
                }
                if ($ExcludeDomainControllers.Count -gt 0) {
                    If (-not $ExcludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -in $ExcludeDomainControllers) {
                            continue
                        }
                    }
                    else {
                        if ($S.HostName -in $ExcludeDomainControllers) {
                            continue
                        }
                    }
                }
                $S
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            }
            else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
            }
            # Building all DCs for whole Forest
            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        $Findings
    }
}
function Start-TimeLog { 
    [CmdletBinding()]
    param()
    [System.Diagnostics.Stopwatch]::StartNew()
}
function Stop-TimeLog { 
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time,
        [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner',
        [switch] $Continue
    )
    Begin {
    }
    Process {
        if ($Option -eq 'Array') {
            $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds"
        }
        else {
            $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        }
    }
    End {
        if (-not $Continue) {
            $Time.Stop()
        }
        return $TimeToExecute
    }
}
function Write-Color { 
    <#
    .SYNOPSIS
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    .DESCRIPTION
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    It provides:
    - Easy manipulation of colors,
    - Logging output to file (log)
    - Nice formatting options out of the box.
    - Ability to use aliases for parameters
 
    .PARAMETER Text
    Text to display on screen and write to log file if specified.
    Accepts an array of strings.
 
    .PARAMETER Color
    Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER BackGroundColor
    Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER StartTab
    Number of tabs to add before text. Default is 0.
 
    .PARAMETER LinesBefore
    Number of empty lines before text. Default is 0.
 
    .PARAMETER LinesAfter
    Number of empty lines after text. Default is 0.
 
    .PARAMETER StartSpaces
    Number of spaces to add before text. Default is 0.
 
    .PARAMETER LogFile
    Path to log file. If not specified no log file will be created.
 
    .PARAMETER DateTimeFormat
    Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss
 
    .PARAMETER LogTime
    If set to $true it will add time to log file. Default is $true.
 
    .PARAMETER LogRetry
    Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2.
 
    .PARAMETER Encoding
    Encoding of the log file. Default is Unicode.
 
    .PARAMETER ShowTime
    Switch to add time to console output. Default is not set.
 
    .PARAMETER NoNewLine
    Switch to not add new line at the end of the output. Default is not set.
 
    .PARAMETER NoConsoleOutput
    Switch to not output to console. Default all output goes to console.
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    Write-Color -t "my text" -c yellow -b green
    Write-Color -text "my text" -c red
 
    .EXAMPLE
    Write-Color -Text "TestujÄ™ czy siÄ™ Å‚adnie zapisze, czy bÄ™dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput
 
    .NOTES
    Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
    Project support: https://github.com/EvotecIT/PSWriteColor
    Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } # Add empty line before
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  # Add TABS before text
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  # Add SPACES before text
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } # Add Time before output
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {
                # the real deal coloring
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                }
                else {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                }
            }
            else {
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline 
                    }
                }
                else {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline 
                    }
                }
            }
        }
        if ($NoNewLine -eq $true) {
            Write-Host -NoNewline 
        }
        else {
            Write-Host 
        } # Support for no new line
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  # Add empty line after
    }
    if ($Text.Count -and $LogFile) {
        # Save to file
        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) {
            $TextToFile += $Text[$i]
        }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) {
                    "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                else {
                    "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                $Saved = $true
            }
            catch {
                if ($Saved -eq $false -and $Retry -eq $LogRetry) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                }
                else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function ConvertTo-OperatingSystem { 
    <#
    .SYNOPSIS
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .DESCRIPTION
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .PARAMETER OperatingSystem
    Operating System as returned by Active Directory
 
    .PARAMETER OperatingSystemVersion
    Operating System Version as returned by Active Directory
 
    .EXAMPLE
    $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object {
        $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
        Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force
        $_
    }
    $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table
 
    .EXAMPLE
    $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $OperatingSystem,
        [string] $OperatingSystemVersion
    )

    if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') {
        $Systems = @{
            # This is how it's written in AD
            '10.0 (22621)' = 'Windows 11 22H2'
            '10.0 (22000)' = 'Windows 11 21H2'
            '10.0 (19045)' = 'Windows 10 22H2'
            '10.0 (19044)' = 'Windows 10 21H2'
            '10.0 (19043)' = 'Windows 10 21H1'
            '10.0 (19042)' = 'Windows 10 20H2'
            '10.0 (19041)' = 'Windows 10 2004'
            '10.0 (18898)' = 'Windows 10 Insider Preview'
            '10.0 (18363)' = "Windows 10 1909"
            '10.0 (18362)' = "Windows 10 1903"
            '10.0 (17763)' = "Windows 10 1809"
            '10.0 (17134)' = "Windows 10 1803"
            '10.0 (16299)' = "Windows 10 1709"
            '10.0 (15063)' = "Windows 10 1703"
            '10.0 (14393)' = "Windows 10 1607"
            '10.0 (10586)' = "Windows 10 1511"
            '10.0 (10240)' = "Windows 10 1507"

            # This is how WMI/CIM stores it
            '10.0.22621'   = 'Windows 11 22H2'
            '10.0.22000'   = 'Windows 11 21H2'
            '10.0.19045'   = 'Windows 10 22H2'
            '10.0.19044'   = 'Windows 10 21H2'
            '10.0.19043'   = 'Windows 10 21H1'
            '10.0.19042'   = 'Windows 10 20H2'
            '10.0.19041'   = 'Windows 10 2004'
            '10.0.18898'   = 'Windows 10 Insider Preview'
            '10.0.18363'   = "Windows 10 1909"
            '10.0.18362'   = "Windows 10 1903"
            '10.0.17763'   = "Windows 10 1809"
            '10.0.17134'   = "Windows 10 1803"
            '10.0.16299'   = "Windows 10 1709"
            '10.0.15063'   = "Windows 10 1703"
            '10.0.14393'   = "Windows 10 1607"
            '10.0.10586'   = "Windows 10 1511"
            '10.0.10240'   = "Windows 10 1507"

            # This is how it's written in registry
            '22621'        = 'Windows 11 22H2'
            '22000'        = 'Windows 11 21H2'
            '19045'        = 'Windows 10 22H2'
            '19044'        = 'Windows 10 21H2'
            '19043'        = 'Windows 10 21H1'
            '19042'        = 'Windows 10 20H2'
            '19041'        = 'Windows 10 2004'
            '18898'        = 'Windows 10 Insider Preview'
            '18363'        = "Windows 10 1909"
            '18362'        = "Windows 10 1903"
            '17763'        = "Windows 10 1809"
            '17134'        = "Windows 10 1803"
            '16299'        = "Windows 10 1709"
            '15063'        = "Windows 10 1703"
            '14393'        = "Windows 10 1607"
            '10586'        = "Windows 10 1511"
            '10240'        = "Windows 10 1507"
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    }
    elseif ($OperatingSystem -like 'Windows Server*') {
        # May need updates https://docs.microsoft.com/en-us/windows-server/get-started/windows-server-release-info
        # to detect Core

        $Systems = @{
            # This is how it's written in AD
            '10.0 (20348)' = 'Windows Server 2022'
            '10.0 (19042)' = 'Windows Server 2019 20H2'
            '10.0 (19041)' = 'Windows Server 2019 2004'
            '10.0 (18363)' = 'Windows Server 2019 1909'
            '10.0 (18362)' = "Windows Server 2019 1903" # (Datacenter Core, Standard Core)
            '10.0 (17763)' = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard)
            '10.0 (17134)' = "Windows Server 2016 1803" # (Datacenter, Standard)
            '10.0 (14393)' = "Windows Server 2016 1607"
            '6.3 (9600)'   = 'Windows Server 2012 R2'
            '6.1 (7601)'   = 'Windows Server 2008 R2'
            '5.2 (3790)'   = 'Windows Server 2003'

            # This is how WMI/CIM stores it
            '10.0.20348'   = 'Windows Server 2022'
            '10.0.19042'   = 'Windows Server 2019 20H2'
            '10.0.19041'   = 'Windows Server 2019 2004'
            '10.0.18363'   = 'Windows Server 2019 1909'
            '10.0.18362'   = "Windows Server 2019 1903" # (Datacenter Core, Standard Core)
            '10.0.17763'   = "Windows Server 2019 1809"  # (Datacenter, Essentials, Standard)
            '10.0.17134'   = "Windows Server 2016 1803" ## (Datacenter, Standard)
            '10.0.14393'   = "Windows Server 2016 1607"
            '6.3.9600'     = 'Windows Server 2012 R2'
            '6.1.7601'     = 'Windows Server 2008 R2' # i think
            '5.2.3790'     = 'Windows Server 2003' # i think

            # This is how it's written in registry
            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" # (Datacenter Core, Standard Core)
            '17763'        = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard)
            '17134'        = "Windows Server 2016 1803" # (Datacenter, Standard)
            '14393'        = "Windows Server 2016 1607"
            '9600'         = 'Windows Server 2012 R2'
            '7601'         = 'Windows Server 2008 R2'
            '3790'         = 'Windows Server 2003'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    }
    else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    }
    else {
        'Unknown'
    }
}
function Copy-DictionaryManual { 
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $clone = @{}
    foreach ($Key in $Dictionary.Keys) {
        $value = $Dictionary.$Key

        $clonedValue = switch ($Dictionary.$Key) {
            { $null -eq $_ } {
                $null
                continue
            }
            { $_ -is [System.Collections.IDictionary] } {
                Copy-DictionaryManual -Dictionary $_
                continue
            }
            {
                $type = $_.GetType()
                $type.IsPrimitive -or $type.IsValueType -or $_ -is [string]
            } {
                $_
                continue
            }
            default {
                $_ | Select-Object -Property *
            }
        }

        if ($value -is [System.Collections.IList]) {
            $clone[$Key] = @($clonedValue)
        }
        else {
            $clone[$Key] = $clonedValue
        }
    }

    $clone
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com1/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdLetBinding()]
    param(
        [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url
    )
    $ProgressPreference = 'SilentlyContinue'

    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{
                    PublishDate = [DateTime]  $JsonContent.published_at
                    CreatedDate = [DateTime] $JsonContent.created_at
                    PreRelease  = [bool] $JsonContent.prerelease
                    Version     = [version] ($JsonContent.name -replace 'v', '')
                    Tag         = $JsonContent.tag_name
                    Branch      = $JsonContent.target_commitish
                    Errors      = ''
                }
            }
        }
        catch {
            [PSCustomObject] @{
                PublishDate = $null
                CreatedDate = $null
                PreRelease  = $null
                Version     = $null
                Tag         = $null
                Branch      = $null
                Errors      = $_.Exception.Message
            }
        }
    }
    else {
        [PSCustomObject] @{
            PublishDate = $null
            CreatedDate = $null
            PreRelease  = $null
            Version     = $null
            Tag         = $null
            Branch      = $null
            Errors      = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Test-ComputerPort { 
    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName,
        [int[]] $PortTCP,
        [int[]] $PortUDP,
        [int]$Timeout = 5000
    )
    begin {
        if ($Global:ProgressPreference -ne 'SilentlyContinue') {
            $TemporaryProgress = $Global:ProgressPreference
            $Global:ProgressPreference = 'SilentlyContinue'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            foreach ($P in $PortTCP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'TCP'
                    'Status'       = $null
                    'Summary'      = $null
                    'Response'     = $null
                }
                <#
                $TcpClient = [System.Net.Sockets.TcpClient]::new()
                $Connect = $TcpClient.BeginConnect($Computer, $P, $null, $null)
                $Wait = $Connect.AsyncWaitHandle.WaitOne($Timeout, $false)
                if (!$Wait) {
                    $TcpClient.Close()
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                } else {
                    $TcpClient.EndConnect($Connect)
                    $TcpClient.Close()
                    $Output['Status'] = $true
                    $Output['Summary'] = "TCP $P Successful"
                }
                $TcpClient.Close()
                $TcpClient.Dispose()
 
 
                #>


                $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue
                if ($TcpClient.TcpTestSucceeded) {
                    $Output['Status'] = $TcpClient.TcpTestSucceeded
                    $Output['Summary'] = "TCP $P Successful"
                }
                else {
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                    $Output['Response'] = $Warnings
                }
                [PSCustomObject]$Output
            }
            foreach ($P in $PortUDP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'UDP'
                    'Status'       = $null
                    'Summary'      = $null
                }
                $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P)
                $UdpClient.Client.ReceiveTimeout = $Timeout
                # $UdpClient.Connect($Computer, $P)
                $Encoding = [System.Text.ASCIIEncoding]::new()
                $byte = $Encoding.GetBytes("Evotec")
                [void]$UdpClient.Send($byte, $byte.length)
                $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
                try {
                    $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint)
                    [string]$Data = $Encoding.GetString($Bytes)
                    If ($Data) {
                        $Output['Status'] = $true
                        $Output['Summary'] = "UDP $P Successful"
                        $Output['Response'] = $Data
                    }
                }
                catch {
                    $Output['Status'] = $false
                    $Output['Summary'] = "UDP $P Failed"
                    $Output['Response'] = $_.Exception.Message
                }
                $UdpClient.Close()
                $UdpClient.Dispose()
                [PSCustomObject]$Output
            }
        }
    }
    end {
        # Bring back setting as per default
        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }
    }
}
function Test-WinRM { 
    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName
    )
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{
            Output       = $null
            Status       = $null
            ComputerName = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        }
        catch {
            $Test.Status = $false
        }
        $Test
    }
    $Output
}
$Script:ReportMailbox = [ordered] @{
    Name       = 'Mailbox'
    Enabled    = $true
    Execute    = {
        Get-MyMailbox
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        New-HTMLTable -DataTable $Script:Reporting['Mailbox']['Data'] -Filtering {
        }
    }
}
$Script:ReportMailboxProblems = [ordered] @{
    Name       = 'MailboxProblems'
    Enabled    = $true
    Execute    = {
        Get-MyMailboxProblems -Local
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['MailboxProblems']['Data'] -is [System.Collections.IDictionary]) {
            New-HTMLTabPanel -Theme forge {
                New-HTMLTab -Name "Duplicate Alias" {
                    New-HTMLSection -HeaderText 'Exchange Online - Duplicate Alias' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Online']['DuplicateAlias'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange On-Premises - Duplicate Alias' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['DuplicateAlias'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange - Duplicate Alias (Together)' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Both']['DuplicateAlias'] -Filtering
                    }
                }
                New-HTMLTab -Name "Duplicate Account" {
                    New-HTMLSection -HeaderText 'Exchange - Duplicate Account' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Both']['DuplicateAccount'] -Filtering
                    }
                }
                New-HTMLTab -Name 'No Database' {
                    New-HTMLSection -HeaderText 'Exchange On-Premises - No Database' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['NoDatabase'] -Filtering
                    }
                }
                New-HTMLTab -Name 'Inconsistent Data' {
                    New-HTMLSection -HeaderText 'Exchange Online - Broken DisplayName' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Online']['BrokenDisplayName'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange On-Premises - Broken DisplayName' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['BrokenDisplayName'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange On-Premises - Missing UserPrincipalName' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['MissingUserPrincipalName'] -Filtering
                    }
                }
                New-HTMLTab -Name 'Contact Problems' {
                    New-HTMLSection -HeaderText 'Exchange Online - Contact Missing PrimarySmtp' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Online']['ContactMissingPrimarySmtp'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange On-Premises - Contact Missing PrimarySmtp' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['ContactMissingPrimarySmtp'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange Online - Contact Missing ExternalEmail' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Online']['ContactMissingExternalEmail'] -Filtering
                    }
                    New-HTMLSection -HeaderText 'Exchange On-Premises - Contact Missing ExternalEmail' {
                        New-HTMLTable -DataTable $Script:Reporting['MailboxProblems']['Data']['Local']['ContactMissingExternalEmail'] -Filtering
                    }
                }
            }
        }
    }
}
function Get-MyMailboxSendAs {
    <#
    .SYNOPSIS
    Short function that returns Send-As permissions for mailbox.
 
    .DESCRIPTION
    Function that returns Send-As permissions for mailbox. It's replacement of Get-ADPermission cmdlet that is very slow and inefficient.
 
    .PARAMETER ADUser
    Active Directory user object
 
    .PARAMETER Identity
    DistinguishedName of mailbox
 
    .EXAMPLE
    Get-ADUser -Identity 'przemyslaw.klys' -Properties NtsecurityDescriptor | Get-MyMailboxSendAs
 
    .EXAMPLE
    $Mailbox = Get-Mailbox -Identity 'przemyslaw.klys'
    $Test = Get-MyMailboxSendAs -Identity $Mailbox.DistinguishedName
    $Test | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory, ParameterSetName = 'ADUser', ValueFromPipeline, ValueFromPipelineByPropertyName)][Microsoft.ActiveDirectory.Management.ADAccount] $ADUser,
        [parameter(Mandatory, ParameterSetName = 'Identity', ValueFromPipeline, ValueFromPipelineByPropertyName )][string] $Identity
    )
    process {
        if ($ADUser) {
            if (-not $ADUser.NtsecurityDescriptor) {
                Write-Warning -Message "Get-MyMailboxSendAs - Identity $($ADUser.SamAccountName) does not have ntSecurityDescriptor attribute. Please provide one or use Identity parameter."
                return
            }
        }
        else {
            if (-not $Script:ForestInformation) {
                $Script:ForestInformation = Get-WinADForestDetails
            }
            $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Identity -ToDomainCN
            $QueryServer = $Script:ForestInformation['QueryServers'][$DomainName].HostName[0]
            $ADUser = Get-ADUser -Identity $Identity -Properties ntSecurityDescriptor -Server $QueryServer
        }
        $ExtendedPermissions = foreach ($Permission in $ADUser.NtsecurityDescriptor.Access) {
            # filter out extended right and object type (Send-As)
            if ($Permission.ActiveDirectoryRights -eq 'ExtendedRight' -and $Permission.objectType -eq "ab721a54-1e2f-11d0-9819-00aa0040529b") {
                [PSCustomObject] @{
                    User         = $Permission.IdentityReference
                    Identity     = if ($ADUser.CanonicalName) {
                        $ADUser.CanonicalName 
                    }
                    else {
                        $ADUser.DistinguishedName 
                    }
                    AccessRights = 'Send-As'
                    Deny         = $Permission.AccessControlType -ne 'Allow'
                    IsInherited  = $Permission.IsInherited
                }
            }
        }
        $ExtendedPermissions
    }
}
function New-HTMLReportExchangeEssentials {
    [cmdletBinding()]
    param(
        [Array] $Type,
        [switch] $Online,
        [switch] $HideHTML,
        [string] $FilePath
    )

    New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys' -TitleText 'ExchangeEssentials Report' {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "ExchangeEssentials - $($Script:Reporting['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }

        if ($Type.Count -eq 1) {
            foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
                if ($Script:ExchangeEssentialsConfiguration[$T].Enabled -eq $true) {
                    if ($Script:ExchangeEssentialsConfiguration[$T]['Summary']) {
                        $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ExchangeEssentialsConfiguration[$T]['Summary']
                    }
                    & $Script:ExchangeEssentialsConfiguration[$T]['Solution']
                }
            }
        }
        else {
            foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
                if ($Script:ExchangeEssentialsConfiguration[$T].Enabled -eq $true) {
                    if ($Script:ExchangeEssentialsConfiguration[$T]['Summary']) {
                        $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ExchangeEssentialsConfiguration[$T]['Summary']
                    }
                    New-HTMLTab -Name $Script:ExchangeEssentialsConfiguration[$T]['Name'] {
                        & $Script:ExchangeEssentialsConfiguration[$T]['Solution']
                    }
                }
            }
        }
    } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath
}
function New-HTMLReportExchangeEssentialsWithSplit {
    [cmdletBinding()]
    param(
        [Array] $Type,
        [switch] $Online,
        [switch] $HideHTML,
        [string] $FilePath,
        [string] $CurrentReport
    )

    # Split reports into multiple files for easier viewing
    $DateName = $(Get-Date -f yyyy-MM-dd_HHmmss)
    $FileName = [io.path]::GetFileNameWithoutExtension($FilePath)
    $DirectoryName = [io.path]::GetDirectoryName($FilePath)

    foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
        if ($Script:ExchangeEssentialsConfiguration[$T].Enabled -eq $true -and ((-not $CurrentReport) -or ($CurrentReport -and $CurrentReport -eq $T))) {
            $NewFileName = $FileName + '_' + $T + "_" + $DateName + '.html'
            $FilePath = [io.path]::Combine($DirectoryName, $NewFileName)

            New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys' -TitleText "ExchangeEssentials $CurrentReport Report" {
                New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
                New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
                New-HTMLPanelStyle -BorderRadius 0px
                New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

                New-HTMLHeader {
                    New-HTMLSection -Invisible {
                        New-HTMLSection {
                            New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                        } -JustifyContent flex-start -Invisible
                        New-HTMLSection {
                            New-HTMLText -Text "ExchangeEssentials - $($Script:Reporting['Version'])" -Color Blue
                        } -JustifyContent flex-end -Invisible
                    }
                }
                if ($Script:ExchangeEssentialsConfiguration[$T]['Summary']) {
                    $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ExchangeEssentialsConfiguration[$T]['Summary']
                }
                & $Script:ExchangeEssentialsConfiguration[$T]['Solution']
            } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath
        }
    }
}
function Reset-ExchangeEssentialsStatus {
    [cmdletBinding()]
    param(

    )
    if (-not $Script:DefaultTypes) {
        $Script:DefaultTypes = foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
            if ($Script:ExchangeEssentialsConfiguration[$T].Enabled) {
                $T
            }
        }
    }
    else {
        foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
            if ($Script:ExchangeEssentialsConfiguration[$T]) {
                $Script:ExchangeEssentialsConfiguration[$T]['Enabled'] = $false
            }
        }
        foreach ($T in $Script:DefaultTypes) {
            if ($Script:ExchangeEssentialsConfiguration[$T]) {
                $Script:ExchangeEssentialsConfiguration[$T]['Enabled'] = $true
            }
        }
    }
}
$Script:ExchangeEssentialsConfiguration = [ordered] @{
    Mailbox         = $Script:ReportMailbox
    MailboxProblems = $Script:ReportMailboxProblems
}
function Get-MyMailbox {
    [CmdletBinding()]
    param(
        [switch] $IncludeStatistics,
        [switch] $IncludeCAS,
        [switch] $Local,
        [int] $LimitProcessing
    )

    $ReversedPermissions = [ordered] @{}
    $CacheMailbox = [ordered] @{}
    $CacheCasMailbox = [ordered] @{}
    $CacheNames = [ordered] @{}
    $FinalOutput = [ordered] @{}
    $CacheType = [ordered] @{}

    $CacheContacts = [ordered] @{}
    $CacheContactsLocal = [ordered] @{}
    $CacheRemoteDomains = [ordered] @{}
    $CacheRecipientPermissions = [ordered] @{}

    Write-Verbose -Message 'Get-MyMailbox - Getting Mailboxes'

    if (-not $Mailboxes) {
        $Mailboxes = @(
            if ($Local) {
                $TimeLog = Start-TimeLog
                Write-Verbose -Message 'Get-MyMailbox - Getting Mailboxes (Local)'
                try {
                    $LocalMailboxes = Get-LocalMailbox -ResultSize unlimited -ErrorAction Stop
                    $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
                    Write-Verbose -Message "Get-MyMailbox - Getting Mailboxes (Local) took $($EndTimeLog)"
                }
                catch {
                    $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
                    Write-Verbose -Message "Get-MyMailbox - Getting Mailboxes (Local) took $($EndTimeLog)"
                    Write-Warning -Message "Get-MyMailbox - Unable to get Mailboxes. Error: $($_.Exception.Message.Replace("`r`n", " "))"
                    return
                }
                # $TimeLog = Start-TimeLog
                foreach ($Mailbox in $LocalMailboxes) {
                    $CacheNames[$Mailbox.UserPrincipalName] = $Mailbox.Alias
                    $CacheNames[$Mailbox.Identity] = $Mailbox.Alias
                    $CacheNames[$Mailbox.Alias] = $Mailbox.Alias
                    $CacheType[$Mailbox.Alias] = 'Local'
                    $Mailbox
                }
                # $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
                # Write-Verbose -Message "Get-MyMailbox - Processing Mailboxes ($($LocalMailboxes.Count)) (Local) took $($EndTimeLog)"
            }
            Write-Verbose -Message 'Get-MyMailbox - Getting Mailboxes (Online)'
            $TimeLog = Start-TimeLog
            try {
                $OnlineMailboxes = Get-EXOMailbox -Properties GrantSendOnBehalfTo, ForwardingSmtpAddress, RecipientTypeDetails, SamAccountName, WhenCreated, WhenMailboxCreated, HiddenFromAddressListsEnabled, ForwardingAddress -ResultSize unlimited -ErrorAction Stop
                $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
                Write-Verbose -Message "Get-MyMailbox - Getting Mailboxes (Online) took $($EndTimeLog) seconds"
            }
            catch {
                $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
                Write-Verbose -Message "Get-MyMailbox - Getting Mailboxes (Online) took $($EndTimeLog) seconds"
                Write-Warning -Message "Get-MyMailbox - Unable to get Mailboxes. Error: $($_.Exception.Message.Replace("`r`n", " "))"
                return
            }
            #$TimeLog = Start-TimeLog
            foreach ($Mailbox in $OnlineMailboxes) {
                $CacheNames[$Mailbox.UserPrincipalName] = $Mailbox.Alias
                $CacheNames[$Mailbox.Identity] = $Mailbox.Alias
                $CacheNames[$Mailbox.Alias] = $Mailbox.Alias
                $CacheType[$Mailbox.Alias] = 'Online'
                $Mailbox
            }
            #$EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
            #Write-Verbose -Message "Get-MyMailbox - Processing Mailboxes ($($OnlineMailboxes.Count)) (Online) took $($EndTimeLog)"
        )
    }
    else {
        Write-Verbose -Message 'Get-MyMailbox - Mailboxes already cached'
        foreach ($Mailbox in $LocalMailboxes) {
            $CacheNames[$Mailbox.UserPrincipalName] = $Mailbox.Alias
            $CacheNames[$Mailbox.Identity] = $Mailbox.Alias
            $CacheNames[$Mailbox.Alias] = $Mailbox.Alias
            $CacheType[$Mailbox.Alias] = 'Local'
        }
        foreach ($Mailbox in $OnlineMailboxes) {
            $CacheNames[$Mailbox.UserPrincipalName] = $Mailbox.Alias
            $CacheNames[$Mailbox.Identity] = $Mailbox.Alias
            $CacheNames[$Mailbox.Alias] = $Mailbox.Alias
            $CacheType[$Mailbox.Alias] = 'Online'
        }
    }

    if (-not $RecipientPermissions) {
        Write-Verbose -Message 'Get-MyMailbox - Getting RecipientPermission'
        try {
            $RecipientPermissions = Get-EXORecipientPermission -ResultSize Unlimited -ErrorAction Stop
        }
        catch {
            Write-Warning -Message "Get-MyMailbox - Unable to get Recipient Permissions. Error: $($_.Exception.Message.Replace("`r`n", " "))"
            return
        }
    }
    if (-not $ContactsLocal) {
        if ($Local) {
            Write-Verbose -Message 'Get-MyMailbox - Getting Local Mail Contacts'
            try {
                [Array] $ContactsLocal = Get-LocalMailContact -ResultSize Unlimited -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Get-MyMailbox - Unable to get Local Mail Contacts. Error: $($_.Exception.Message.Replace("`r`n", " "))"
            }
        }
    }
    if (-not $Contacts) {
        Write-Verbose -Message 'Get-MyMailbox - Getting Mail Contacts'
        try {
            [Array] $Contacts = Get-MailContact -ResultSize Unlimited -ErrorAction Stop
        }
        catch {
            Write-Warning -Message "Get-MyMailbox - Unable to get Mail Contacts. Error: $($_.Exception.Message.Replace("`r`n", " "))"
            return
        }
    }
    if (-not $RemoteDomains) {
        try {
            $RemoteDomains = @(
                Write-Verbose -Message 'Get-MyMailbox - Getting Remote Domains'
                Get-RemoteDomain -ResultSize Unlimited -ErrorAction Stop
                if ($Local) {
                    Write-Verbose -Message 'Get-MyMailbox - Getting Local Remote Domains'
                    Get-LocalRemoteDomain -ErrorAction Stop
                }
            )
        }
        catch {
            Write-Warning -Message "Get-MyMailbox - Unable to get Remote Domains. Error: $($_.Exception.Message.Replace("`r`n", " "))"
            return
        }
    }
    foreach ($C in $Contacts) {
        $CacheContacts[$C.Identity] = $C
    }
    foreach ($C in $ContactsLocal) {
        $CacheContactsLocal[$C.Identity] = $C
    }
    foreach ($Domain in $RemoteDomains) {
        $CacheRemoteDomains[$Domain.DomainName] = $Domain
    }
    foreach ($RecipientPermission in $RecipientPermissions) {
        if ($RecipientPermission.Trustee -ne 'NT AUTHORITY\SELF') {
            if (-not $CacheRecipientPermissions[$RecipientPermission.Identity]) {
                $CacheRecipientPermissions[$RecipientPermission.Identity] = [System.Collections.Generic.List[PSCustomobject]]::new()
            }
            $CacheRecipientPermissions[$RecipientPermission.Identity].Add($RecipientPermission)
        }
    }
    if ($IncludeCAS) {
        Write-Verbose -Message 'Get-MyMailbox - Getting CAS Mailboxes'
        try {
            if (-not $CasMailboxes) {
                $CasMailboxes = @(
                    Write-Verbose -Message 'Get-MyMailbox - Getting CAS Mailboxes (Online)'
                    Get-CasMailbox -ResultSize unlimited -ErrorAction Stop
                    if ($Local) {
                        Write-Verbose -Message 'Get-MyMailbox - Getting CAS Mailboxes (Local)'
                        Get-LocalCasMailbox -ResultSize unlimited -ErrorAction Stop
                    }
                )
            }
        }
        catch {
            Write-Warning -Message "Get-MyMailbox - Unable to get CasMailboxes. Error: $($_.Exception.Message.Replace("`r`n", " "))"
            return
        }
        foreach ($Mailbox in $CasMailboxes) {
            $CacheCasMailbox[$Mailbox.Identity] = $Mailbox
        }
    }
    $Count = 0

    $FilterdMailboxes = @(
        if ($LimitProcessing) {
            $LocalMailboxes | Select-Object -First $LimitProcessing
            $OnlineMailboxes | Select-Object -First $LimitProcessing
        }
        else {
            $Mailboxes
        }
    )
    foreach ($Mailbox in $FilterdMailboxes) {
        $Count++
        Write-Verbose -Message "Processing Mailbox $Count/$($Mailboxes.Count) - $($Mailbox.Alias) / $($Mailbox.UserPrincipalName) / $($Mailbox.DisplayName)"
        $TimeLog = Start-TimeLog
        $CacheMailbox[$Mailbox.Alias] = [ordered] @{
            Mailbox = $Mailbox
        }
        $TimeLogPermissions = Start-TimeLog
        if ($CacheType[$Mailbox.Alias] -eq 'Local') {
            try {
                Write-Verbose -Message "Get-MyMailbox - Getting MailboxPermissions for $($Mailbox.Alias) - Local"
                $CacheMailbox[$Mailbox.Alias].MailboxPermissions = Get-LocalMailboxPermission -Identity $Mailbox.Alias -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Get-MyMailbox - Unable to get MailboxPermissions for $($Mailbox.Alias). Error: $($_.Exception.Message.Replace("`r`n", " "))"
            }
        }
        else {
            try {
                Write-Verbose -Message "Get-MyMailbox - Getting MailboxPermissions for $($Mailbox.Alias) - Online"
                $CacheMailbox[$Mailbox.Alias].MailboxPermissions = Get-MailboxPermission -Identity $Mailbox.Alias -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Get-MyMailbox - Unable to get MailboxPermissions for $($Mailbox.Alias). Error: $($_.Exception.Message.Replace("`r`n", " "))"
            }
        }
        $TimeLogPermissionsEnd = Stop-TimeLog -Time $TimeLogPermissions
        $TimeLogRecipient = Start-TimeLog
        if ($CacheType[$Mailbox.Alias] -eq 'Local') {
            try {
                Write-Verbose -Message "Get-MyMailbox - Getting MailboxADPermissions for $($Mailbox.Alias) - Local"
                $CacheMailbox[$Mailbox.Alias].MailboxRecipientPermissions = Get-MyMailboxSendAs -Identity $Mailbox.DistinguishedName #Get-LocalADPermission -Identity $Mailbox.Identity -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Get-MyMailbox - Unable to get MailboxADPermissions for $($Mailbox.Alias). Error: $($_.Exception.Message.Replace("`r`n", " "))"
            }
        }
        else {
            if ($CacheRecipientPermissions[$Mailbox.Identity] -and $CacheRecipientPermissions[$Mailbox.Identity].Count -gt 0) {
                $CacheMailbox[$Mailbox.Alias].MailboxRecipientPermissions = $CacheRecipientPermissions[$Mailbox.Identity]
            }
            # try {
            # Write-Verbose -Message "Get-MyMailbox - Getting MailboxRecipientPermissions for $($Mailbox.Alias) - Online"
            # $CacheMailbox[$Mailbox.Alias].MailboxRecipientPermissions = Get-RecipientPermission -Identity $Mailbox.Alias -ErrorAction Stop
            # } catch {
            # Write-Warning -Message "Get-MyMailbox - Unable to get MailboxRecipientPermissions for $($Mailbox.Alias). Error: $($_.Exception.Message.Replace("`r`n", " "))"
            # }
        }
        $TimeLogRecipientEnd = Stop-TimeLog -Time $TimeLogRecipient
        $TImeLogStats = Start-TimeLog
        if ($IncludeStatistics) {
            if ($CacheType[$Mailbox.Alias] -eq 'Local') {
                $CacheMailbox[$Mailbox.Alias]['Statistics'] = Get-LocalMailboxStatistics -Identity $Mailbox.Alias
            }
            else {
                $CacheMailbox[$Mailbox.Alias]['Statistics'] = Get-MailboxStatistics -Identity $Mailbox.Alias
            }
            if ($Mailbox.ArchiveDatabase) {
                try {
                    if ($CacheType[$Mailbox.Alias] -eq 'Local') {
                        $Archive = Get-LocalMailboxStatistics -Identity ($Mailbox.Guid).ToString() -Archive -Verbose:$false -ErrorAction Stop
                    }
                    else {
                        $Archive = Get-MailboxStatistics -Identity ($Mailbox.Guid).ToString() -Archive -Verbose:$false -ErrorAction Stop
                    }
                    $CacheMailbox[$Mailbox.Alias]['StatisticsArchive'] = $Archive
                }
                catch {
                    Write-Warning -Message "Get-MyMailbox - Unable to get ArchiveStatistics for $($Mailbox.Alias). Error: $($_.Exception.Message.Replace("`r`n", " "))"
                }
            }
        }
        $TimeLogStatsEnd = Stop-TimeLog -Time $TImeLogStats
        $TimeLogProcessing = Start-TimeLog
        foreach ($Permission in $CacheMailbox[$Mailbox.Alias].MailboxPermissions) {
            if ($Permission.Deny -eq $false) {
                if ($Permission.User -ne 'NT AUTHORITY\SELF') {
                    if ($CacheType[$Mailbox.Alias] -eq 'Local') {
                        $UserSplit = $Permission.User.Split("\")
                        $CurrentUser = $UserSplit[1]
                    }
                    else {
                        $CurrentUser = $CacheNames[$Permission.User]
                    }
                    if ($CurrentUser) {
                        foreach ($Right in $Permission.AccessRights) {
                            if ($Right -like 'FullAccess*') {
                                if (-not $ReversedPermissions[$CurrentUser]) {
                                    $ReversedPermissions[$CurrentUser] = [ordered] @{
                                        FullAccess   = [System.Collections.Generic.List[string]]::new()
                                        SendAs       = [System.Collections.Generic.List[string]]::new()
                                        SendOnBehalf = [System.Collections.Generic.List[string]]::new()
                                    }
                                }
                                $ReversedPermissions[$CurrentUser].FullAccess.Add($Mailbox.Alias)
                            }
                        }
                    }
                    else {
                        # Write-Warning -Message "Unable to process $($Permission.User) for $($Mailbox.Alias)"
                    }
                }
            }
        }
        foreach ($Permission in $CacheMailbox[$Mailbox.Alias].MailboxRecipientPermissions) {
            if ($CacheType[$Mailbox.Alias] -eq 'Local') {
                if ($Permission.Deny -eq $false -and $Permission.Inherited -eq $false) {
                    if ($Permission.User -ne 'NT AUTHORITY\SELF') {
                        $UserSplit = $Permission.User.Split("\")
                        $CurrentUser = $UserSplit[1]
                        if ($CurrentUser) {
                            foreach ($Right in $Permission.AccessRights) {
                                if (($Right -like 'Send*')) {
                                    if (-not $ReversedPermissions[$CurrentUser]) {
                                        $ReversedPermissions[$CurrentUser] = [ordered] @{
                                            FullAccess   = [System.Collections.Generic.List[string]]::new()
                                            SendAs       = [System.Collections.Generic.List[string]]::new()
                                            SendOnBehalf = [System.Collections.Generic.List[string]]::new()
                                        }
                                    }
                                    $ReversedPermissions[$CurrentUser].SendAs.Add($Mailbox.Alias)
                                }
                            }
                        }
                        else {
                            #Write-Warning -Message "Unable to process $($Permission.Trustee) for $($Mailbox.Alias)"
                        }
                    }
                }
            }
            else {
                if ($Permission.AccessControlType -eq 'Allow') {
                    if ($Permission.Trustee -ne 'NT AUTHORITY\SELF') {
                        $CurrentUser = $CacheNames[$Permission.Trustee]
                        if ($CurrentUser) {
                            foreach ($Right in $Permission.AccessRights) {
                                if (($Right -like 'Send*')) {
                                    if (-not $ReversedPermissions[$CurrentUser]) {
                                        $ReversedPermissions[$CurrentUser] = [ordered] @{
                                            FullAccess   = [System.Collections.Generic.List[string]]::new()
                                            SendAs       = [System.Collections.Generic.List[string]]::new()
                                            SendOnBehalf = [System.Collections.Generic.List[string]]::new()
                                        }
                                    }
                                    $ReversedPermissions[$CurrentUser].SendAs.Add($Mailbox.Alias)
                                }
                            }
                        }
                        else {
                            #Write-Warning -Message "Unable to process $($Permission.Trustee) for $($Mailbox.Alias)"
                        }
                    }
                }
            }
        }
        foreach ($Permission in $CacheMailbox[$Mailbox.Alias].Mailbox.GrantSendOnBehalfTo) {
            $CurrentUser = $CacheNames[$Permission]
            if ($CurrentUser) {
                if (-not $ReversedPermissions[$CurrentUser]) {
                    $ReversedPermissions[$CurrentUser] = [ordered] @{
                        FullAccess   = [System.Collections.Generic.List[string]]::new()
                        SendAs       = [System.Collections.Generic.List[string]]::new()
                        SendOnBehalf = [System.Collections.Generic.List[string]]::new()
                    }
                }

                $ReversedPermissions[$CurrentUser].SendOnBehalf.Add($Mailbox.Alias)
            }
            else {
                # Write-Warning -Message "Unable to process $($Permission) for $($Mailbox.Alias)"
            }
        }
        $TimeLogProcessingEnd = Stop-TimeLog -Time $TimeLogProcessing
        $EndTimeLog = Stop-TimeLog -Time $TimeLog -Option OneLiner
        Write-Verbose -Message "Processing Mailbox $Count/$($Mailboxes.Count) - $($Mailbox.Alias) / $($Mailbox.UserPrincipalName) / $($Mailbox.DisplayName) - [$TimeLogPermissionsEnd][$TimeLogRecipientEnd][$TimeLogStatsEnd][$TimeLogProcessingEnd]"
        Write-Verbose -Message "Processing Mailbox $Count/$($Mailboxes.Count) - $($Mailbox.Alias) / $($Mailbox.UserPrincipalName) / $($Mailbox.DisplayName) - [$EndTimeLog]"
    }
    foreach ($Alias in $ReversedPermissions.Keys) {
        if ($CacheMailbox[$Alias]) {
            $CacheMailbox[$Alias].FullAccess = $ReversedPermissions[$Alias].FullAccess
            $CacheMailbox[$Alias].SendAs = $ReversedPermissions[$Alias].SendAs
            $CacheMailbox[$Alias].SendOnBehalf = $ReversedPermissions[$Alias].SendOnBehalf
        }
    }
    $Count = 0
    foreach ($Mailbox in $FilterdMailboxes) {
        $Count++
        Write-Verbose -Message "Processing Mailbox $Count/$($FilterdMailboxes.Count) - $($Mailbox.Alias) / $($Mailbox.UserPrincipalName) / $($Mailbox.DisplayName)"
        if ($Mailbox.ForwardingAddress) {
            $Contact = $CacheContacts[$Mailbox.ForwardingAddress]
            $ContactLocal = $CacheContactsLocal[$Mailbox.ForwardingAddress]
            if ($Contact) {
                $ForwardAddress = Convert-ExchangeEmail -Emails $Contact.ExternalEmailAddress -RemovePrefix
                if ($ForwardAddress) {
                    $IsForward = $true
                }
                else {
                    $IsForward = $false
                }
            }
            elseif ($ContactLocal) {
                $ForwardAddress = Convert-ExchangeEmail -Emails $ContactLocal.ExternalEmailAddress -RemovePrefix
                if ($ForwardAddress) {
                    $IsForward = $true
                }
                else {
                    $IsForward = $false
                }
            }
            else {
                $ForwardAddress = $Mailbox.ForwardingAddress
                $IsForward = $true
            }
            $ForwardingType = 'Contact'
        }
        elseif ($Mailbox.ForwardingSmtpAddress) {
            $ForwardAddress = Convert-ExchangeEmail -Emails $Mailbox.ForwardingSmtpAddress -RemovePrefix
            if ($ForwardAddress) {
                $IsForward = $true
            }
            else {
                # this shouldn't happen
                $IsForward = 'Unknown'
            }
            $ForwardingType = 'SmtpAddress'
        }
        else {
            $ForwardAddress = $null
            $IsForward = $false
            $ForwardingType = 'None'
        }
        if ($ForwardAddress) {
            if ($ForwardAddress -like "*@*") {
                $SplitAddress = $ForwardAddress -split "@"
                $DomainName = $SplitAddress[1]
                $DomainName = $DomainName.Trim()
                if ($CacheRemoteDomains[$DomainName]) {
                    $ForwardingStatus = "Internal"
                }
                else {
                    $ForwardingStatus = "External"
                }
            }
            else {
                $ForwardingStatus = "Unknown"
            }
        }
        else {
            $ForwardingStatus = 'None'
        }


        $User = [ordered] @{
            DisplayName                   = $Mailbox.DisplayName
            Alias                         = $Mailbox.Alias
            UserPrincipalName             = $Mailbox.UserPrincipalName
            Enabled                       = -not $Mailbox.AccountDisabled
            Type                          = $CacheType[$Mailbox.Alias]
            TypeDetails                   = $Mailbox.RecipientTypeDetails
            PrimarySmtpAddress            = $Mailbox.PrimarySmtpAddress
            SamAccountName                = $Mailbox.SamAccountName
            ForwardingEnabled             = $IsForward
            ForwardingStatus              = $ForwardingStatus
            ForwardingType                = $ForwardingType
            ForwardingAddress             = $ForwardAddress

            FullAccess                    = $CacheMailbox[$Mailbox.Alias].FullAccess
            SendAs                        = $CacheMailbox[$Mailbox.Alias].SendAs
            SendOnBehalf                  = $CacheMailbox[$Mailbox.Alias].SendOnBehalf
            FullAccessCount               = $CacheMailbox[$Mailbox.Alias].FullAccess.Count
            SendAsCount                   = $CacheMailbox[$Mailbox.Alias].SendAs.Count
            SendOnBehalfCount             = $CacheMailbox[$Mailbox.Alias].SendOnBehalf.Count
            WhenCreated                   = $Mailbox.WhenCreated
            WhenMailboxCreated            = $Mailbox.WhenMailboxCreated
            HiddenFromAddressListsEnabled = $Mailbox.HiddenFromAddressListsEnabled
        }
        if ($IncludeStatistics) {
            #$User['LastUserAccessTime'] = $CacheMailbox[$Mailbox.Alias].Statistics.LastUserAccessTime
            $User['LastLogonTime'] = $CacheMailbox[$Mailbox.Alias].Statistics.LastLogonTime
            $User['TotalItems'] = $CacheMailbox[$Mailbox.Alias].Statistics.ItemCount
            $User['TotalGB'] = Convert-ExchangeSize -Size $CacheMailbox[$Mailbox.Alias].Statistics.TotalItemSize -To GB
            $User['TotalArchiveItems'] = $CacheMailbox[$Mailbox.Alias].StatisticsArchive.ItemCount
            $User['TotalArchiveGB'] = Convert-ExchangeSize -Size $CacheMailbox[$Mailbox.Alias].StatisticsArchive.TotalItemSize -To GB
        }
        if ($IncludeCAS) {
            $CasProperties = @(
                'ActiveSyncEnabled'
                'OWAEnabled'
                'OWAforDevicesEnabled'
                'ECPEnabled'
                'PopEnabled'
                'PopMessageDeleteEnabled'
                'ImapEnabled'
                'MAPIEnabled'
                'MapiHttpEnabled'
                'UniversalOutlookEnabled'
                'OutlookMobileEnabled'
                'MacOutlookEnabled'
                'EwsEnabled'
                'OneWinNativeOutlookEnabled'
                'BulkMailEnabled'
                'SmtpClientAuthenticationDisabled'
            )
            if ($CacheCasMailbox[$Mailbox.Identity]) {
                foreach ($Property in $CasProperties) {
                    $User[$Property] = $CacheCasMailbox[$Mailbox.Alias].$Property
                }
            }
            else {
                foreach ($Property in $CasProperties) {
                    $User[$Property] = $null
                }
            }
        }
        $ConvertedUser = [PSCustomObject] $User

        $FinalOutput[$Mailbox.Alias] = $ConvertedUser
        $ConvertedUser
    }
}
function Get-MyMailboxProblems {
    [CmdletBinding()]
    param(
        [switch] $Local
    )
    $Problems = [ordered] @{
        Data   = [ordered] @{
            ContactsLocal   = $Null
            ContactsOnline  = $Null
            MailboxesLocal  = $Null
            MailboxesOnline = $Null
        }
        Online = [ordered] @{
            BrokenDisplayName           = [System.Collections.Generic.List[PSCustomObject]]::new()
            DuplicateAlias              = [System.Collections.Generic.List[PSCustomObject]]::new()
            NoDatabase                  = [System.Collections.Generic.List[PSCustomObject]]::new()
            ContactMissingPrimarySmtp   = [System.Collections.Generic.List[PSCustomObject]]::new()
            ContactMissingExternalEmail = [System.Collections.Generic.List[PSCustomObject]]::new()
            MissingUserPrincipalName    = [System.Collections.Generic.List[PSCustomObject]]::new()
        }
        Local  = [ordered] @{
            BrokenDisplayName           = [System.Collections.Generic.List[PSCustomObject]]::new()
            DuplicateAlias              = [System.Collections.Generic.List[PSCustomObject]]::new()
            NoDatabase                  = [System.Collections.Generic.List[PSCustomObject]]::new()
            ContactMissingPrimarySmtp   = [System.Collections.Generic.List[PSCustomObject]]::new()
            ContactMissingExternalEmail = [System.Collections.Generic.List[PSCustomObject]]::new()
            MissingUserPrincipalName    = [System.Collections.Generic.List[PSCustomObject]]::new()
        }
        Both   = [ordered] @{
            DuplicateAlias   = [System.Collections.Generic.List[PSCustomObject]]::new()
            DuplicateAccount = [System.Collections.Generic.List[PSCustomObject]]::new()
        }
    }

    $PropertiesMailbox = @(
        'UserPrincipalName', 'Alias', 'DisplayName', 'Database', 'PrimarySmtpAddress', 'RecipientType', 'RecipientTypeDetails', 'ExchangeUserAccountControl' #, 'Identity'
    )
    if ($Local) {
        try {
            $ContactsLocal = Get-LocalMailContact -ResultSize Unlimited -ErrorAction Stop
        }
        catch {
            Write-Warning "Get-MyMailboxProblems - Unable to get local contacts: $($_.Exception.Message)"
            $ContactsLocal = @()
        }
        try {
            $MailboxesLocal = Get-LocalMailbox -ResultSize Unlimited | Select-Object -Property $PropertiesMailbox -ErrorAction Stop
        }
        catch {
            Write-Warning "Get-MyMailboxProblems - Unable to get local mailboxes: $($_.Exception.Message)"
            $MailboxesLocal = @()
        }
    }
    try {
        $ContactsOnline = Get-LocalMailContact -ResultSize Unlimited -ErrorAction Stop
    }
    catch {
        Write-Warning "Get-MyMailboxProblems - Unable to get online contacts: $($_.Exception.Message)"
        $ContactsOnline = @()
    }
    try {
        $MailboxesOnline = Get-EXOMailbox -ResultSize Unlimited -Properties $PropertiesMailbox | Select-Object -Property $PropertiesMailbox -ErrorAction Stop
    }
    catch {
        Write-Warning "Get-MyMailboxProblems - Unable to get online mailboxes: $($_.Exception.Message)"
        $MailboxesOnline = @()
    }
    foreach ($C in $ContactsLocal) {
        if (-not $C.PrimarySmtpAddress) {
            $Problems.Local.ContactMissingPrimarySmtp.Add($C)
        }
        if (-not $C.ExternalEmailAddress) {
            $Problems.Local.ContactMissingExternalEmail.Add($C)
        }
    }
    foreach ($C in $ContactsOnline) {
        if (-not $C.PrimarySmtpAddress) {
            $Problems.Online.ContactMissingPrimarySmtp.Add($C)
        }
        if (-not $C.ExternalEmailAddress) {
            $Problems.Online.ContactMissingExternalEmail.Add($C)
        }
    }

    foreach ($M in $MailboxesLocal) {
        if ($M.DisplayName.StartsWith(' ') -or $M.DisplayName.EndsWith(' ')) {
            $Problems.Local.BrokenDisplayName.Add($M)
        }
        if ($Null -eq $M.Database) {
            $Problems.Local.NoDatabase.Add($M)
        }
        if ($null -eq $M.UserPrincipalName) {
            $Problems.Local.MissingUserPrincipalName.Add($M)
        }
    }
    foreach ($M in $MailboxesOnline) {
        if ($M.DisplayName.StartsWith(' ') -or $M.DisplayName.EndsWith(' ')) {
            $Problems.Online.BrokenDisplayName.Add($M)
        }
        if ($Null -eq $M.Database) {
            $Problems.Online.NoDatabase.Add($M)
        }
        if ($null -eq $M.UserPrincipalName) {
            $Problems.Local.MissingUserPrincipalName.Add($M)
        }
    }

    $CacheAll = [ordered] @{}
    $Cache = [ordered] @{}
    foreach ($M in $MailboxesOnline) {
        if (-not $Cache[$M.Alias]) {
            $Cache[$M.Alias] = $M
        }
        else {
            if (-not $CacheAll[$M.Alias]) {
                $CacheAll[$M.Alias] = [System.Collections.Generic.List[PSCustomObject]]::new()
                $CacheAll[$M.Alias].Add($Cache[$M.Alias])
            }
            $CacheAll[$M.Alias].Add($M)
        }
    }
    foreach ($M in $CacheAll.Values) {
        $Problems.Online.DuplicateAlias.Add($M)
    }
    # lets reset for local
    $CacheAll = [ordered] @{}
    $Cache = [ordered] @{}
    foreach ($M in $MailboxesLocal) {
        if (-not $Cache[$M.Alias]) {
            $Cache[$M.Alias] = $M
        }
        else {
            if (-not $CacheAll[$M.Alias]) {
                $CacheAll[$M.Alias] = [System.Collections.Generic.List[PSCustomObject]]::new()
                $CacheAll[$M.Alias].Add($Cache[$M.Alias])
            }
            $CacheAll[$M.Alias].Add($M)
        }
    }
    foreach ($M in $CacheAll.Values) {
        $Problems.Local.DuplicateAlias.Add($M)
    }
    # both?
    $CacheAll = [ordered] @{}
    $Cache = [ordered] @{}
    $CacheUPN = [ordered] @{}
    $CacheAllUPN = [ordered] @{}
    $Mailboxes = @(
        if ($MailboxesOnline.Count -gt 0) {
            $MailboxesOnline
        }
        if ($MailboxesLocal.Count -gt 0) {
            $MailboxesLocal
        }
    )
    foreach ($M in $Mailboxes) {
        # Duplicate Accounts
        if (-not $CacheUPN[$M.PrimarySmtpAddress]) {
            $CacheUPN[$M.PrimarySmtpAddress] = $M
        }
        else {
            if (-not $CacheAllUPN[$M.PrimarySmtpAddress]) {
                $CacheAllUPN[$M.PrimarySmtpAddress] = [System.Collections.Generic.List[PSCustomObject]]::new()
                $CacheAllUPN[$M.PrimarySmtpAddress].Add($CacheUPN[$M.PrimarySmtpAddress])
            }
            $CacheAllUPN[$M.PrimarySmtpAddress].Add($M)
        }
        # Duplicate Aliases
        if (-not $Cache[$M.Alias]) {
            $Cache[$M.Alias] = $M
        }
        else {
            if (-not $CacheAll[$M.Alias]) {
                $CacheAll[$M.Alias] = [System.Collections.Generic.List[PSCustomObject]]::new()
                $CacheAll[$M.Alias].Add($Cache[$M.Alias])
            }
            $CacheAll[$M.Alias].Add($M)
        }
    }
    foreach ($M in $CacheAll.Values) {
        $Problems.Both.DuplicateAlias.Add($M)
    }
    foreach ($M in $CacheAllUPN.Values) {
        $Problems.Both.DuplicateAccount.Add($M)
    }

    if ($Local) {
        $Problems.Data.ContactsLocal = $ContactsLocal
        $Problems.Data.MailboxesLocal = $MailboxesLocal
    }
    $Problems.Data.ContactsOnline = $ContactsOnline
    $Problems.Data.MailboxesOnline = $MailboxesOnline

    $Problems
}
function Invoke-ExchangeEssentials {
    [CmdletBinding()]
    param(
        [string] $FilePath,
        [Parameter(Position = 0)][string[]] $Type,
        [switch] $PassThru,
        [switch] $HideHTML,
        [switch] $Online,
        [switch] $SplitReports
    )
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-ExchangeEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'ExchangeEssentials'
    $Script:Reporting['Settings'] = @{
        ShowError   = $ShowError.IsPresent
        ShowWarning = $ShowWarning.IsPresent
        HideSteps   = $HideSteps.IsPresent
    }

    Write-Color '[i]', "[ExchangeEssentials] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta

    $Supported = [System.Collections.Generic.List[string]]::new()
    [Array] $NotSupported = foreach ($T in $Type) {
        if ($T -notin $Script:ExchangeEssentialsConfiguration.Keys ) {
            $T
        }
        else {
            $Supported.Add($T)
        }
    }
    if ($Supported) {
        Write-Color '[i]', "[ExchangeEssentials] ", 'Supported types', ' [Informative] ', "Chosen by user: ", ($Supported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
    }
    if ($NotSupported) {
        Write-Color '[i]', "[ExchangeEssentials] ", 'Not supported types', ' [Informative] ', "Following types are not supported: ", ($NotSupported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
        Write-Color '[i]', "[ExchangeEssentials] ", 'Not supported types', ' [Informative] ', "Please use one/multiple from the list: ", ($Script:ExchangeEssentialsConfiguration.Keys -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
        return
    }

    # Lets make sure we only enable those types which are requestd by user
    if ($Type) {
        foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
            $Script:ExchangeEssentialsConfiguration[$T].Enabled = $false
        }
        # Lets enable all requested ones
        foreach ($T in $Type) {
            $Script:ExchangeEssentialsConfiguration[$T].Enabled = $true
        }
    }

    # Build data
    foreach ($T in $Script:ExchangeEssentialsConfiguration.Keys) {
        if ($Script:ExchangeEssentialsConfiguration[$T].Enabled -eq $true) {
            $Script:Reporting[$T] = [ordered] @{
                Name              = $Script:ExchangeEssentialsConfiguration[$T].Name
                ActionRequired    = $null
                Data              = $null
                Exclusions        = $null
                WarningsAndErrors = $null
                Time              = $null
                Summary           = $null
                Variables         = Copy-Dictionary -Dictionary $Script:ExchangeEssentialsConfiguration[$T]['Variables']
            }
            if ($Exclusions) {
                if ($Exclusions -is [scriptblock]) {
                    $Script:Reporting[$T]['ExclusionsCode'] = $Exclusions
                }
                if ($Exclusions -is [Array]) {
                    $Script:Reporting[$T]['Exclusions'] = $Exclusions
                }
            }

            $TimeLogExchangeEssentials = Start-TimeLog
            Write-Color -Text '[i]', '[Start] ', $($Script:ExchangeEssentialsConfiguration[$T]['Name']) -Color Yellow, DarkGray, Yellow
            $OutputCommand = Invoke-Command -ScriptBlock $Script:ExchangeEssentialsConfiguration[$T]['Execute'] -WarningVariable CommandWarnings -ErrorVariable CommandErrors -ArgumentList $Forest, $ExcludeDomains, $IncludeDomains
            if ($OutputCommand -is [System.Collections.IDictionary]) {
                # in some cases the return will be wrapped in Hashtable/orderedDictionary and we need to handle this without an array
                $Script:Reporting[$T]['Data'] = $OutputCommand
            }
            else {
                # since sometimes it can be 0 or 1 objects being returned we force it being an array
                $Script:Reporting[$T]['Data'] = [Array] $OutputCommand
            }
            Invoke-Command -ScriptBlock $Script:ExchangeEssentialsConfiguration[$T]['Processing']
            $Script:Reporting[$T]['WarningsAndErrors'] = @(
                if ($ShowWarning) {
                    foreach ($War in $CommandWarnings) {
                        [PSCustomObject] @{
                            Type       = 'Warning'
                            Comment    = $War
                            Reason     = ''
                            TargetName = ''
                        }
                    }
                }
                if ($ShowError) {
                    foreach ($Err in $CommandErrors) {
                        [PSCustomObject] @{
                            Type       = 'Error'
                            Comment    = $Err
                            Reason     = $Err.CategoryInfo.Reason
                            TargetName = $Err.CategoryInfo.TargetName
                        }
                    }
                }
            )
            $TimeEndExchangeEssentials = Stop-TimeLog -Time $TimeLogExchangeEssentials -Option OneLiner
            $Script:Reporting[$T]['Time'] = $TimeEndExchangeEssentials
            Write-Color -Text '[i]', '[End ] ', $($Script:ExchangeEssentialsConfiguration[$T]['Name']), " [Time to execute: $TimeEndExchangeEssentials]" -Color Yellow, DarkGray, Yellow, DarkGray

            if ($SplitReports) {
                Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for ', $T -Color Yellow, DarkGray, Yellow
                $TimeLogHTML = Start-TimeLog
                New-HTMLReportExchangeEssentialsWithSplit -FilePath $FilePath -Online:$Online -HideHTML:$HideHTML -CurrentReport $T
                $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner
                Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for', $T, " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray
            }
        }
    }
    if ( -not $SplitReports) {
        Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report' -Color Yellow, DarkGray, Yellow
        $TimeLogHTML = Start-TimeLog
        if (-not $FilePath) {
            $FilePath = Get-FileName -Extension 'html' -Temporary
        }
        New-HTMLReportExchangeEssentials -Type $Type -Online:$Online.IsPresent -HideHTML:$HideHTML.IsPresent -FilePath $FilePath
        $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner
        Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report', " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray
    }
    Reset-ExchangeEssentialsStatus
    if ($PassThru) {
        $Script:Reporting
    }
}

[scriptblock] $SourcesAutoCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $Script:ExchangeEssentialsConfiguration.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName Invoke-ExchangeEssentials -ParameterName Type -ScriptBlock $SourcesAutoCompleter

if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 379893) {
    Write-Warning "This module requires .NET Framework 4.5.2 or later."; return 
} 

# Export functions and aliases as required
Export-ModuleMember -Function @('Get-MyMailbox', 'Get-MyMailboxProblems', 'Invoke-ExchangeEssentials') -Alias @()
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBHRjEKgrFHEJuB
# wgV1kZgtduxKgC+zi4FfaAmTCOEa46CCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCDShkCGuV4RuGmZnNI79L3qihzVylbA2RKHOMDBRGxTLjAN
# BgkqhkiG9w0BAQEFAASCAgCTCU61R3P65pvH6B0wXOFzrULU/gGNeffTg5tcF7zb
# JjtvmfHi9tqh6dx76k2hdK5lf47UH33lrOHQy7NQyIZk9BQA+wQc6tiKe3HfMWqJ
# 1zwzo8YOUXr99ixnYlNF1R5VfgQ642O4qFz/Es8RGKTUcVpprgHV91WEVdoSladN
# rimeYsK9/bV7WD7lZ4uwGEe1m/sVpbEBbj98nqTA5vDgduI69AFmtEmag9lnkbh2
# WJRzSVIwwFwL8+9sNrNY5iG/nCpZaNu0wmQ/MBJ7coDMi+DGVgUm5WC6rm0mgMSo
# yMg3gSOIIOP0ZcYB5gg0hiJGt2+A0VdxMxJdwcHhjBqNNa3g6iYmlVJbMqnmXTq7
# hv3+itarc0jup5nL6hs3Y+FTwm14d5N2vX1SQ4uBl2V/o/2EBqF90EnPj3rErI87
# t78WrLxgAbYMhLvLA4KR2U6Yv1fWn5sFGz1IMVFINVqIsjQ4GLbYP6RuABwK3NF0
# V3t9cYudYUx5jBZfJVJ0z3md5vr59leO//1nnHMM/kbH7abkHegDeE6avGk0iZOi
# szT83i8fb0JnZamyzaAS8yLTqv6ex5Z3Nnk1+JANK34SIDwh6jE3FByQVfwyPDa5
# tYroAVyRgxRIcGywVv04In8FflNLDDgAGRVWJjyA3KsQ8WOrZU6B4O57xc7wzmv/
# iqGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA4MTExNDIyMTNaMC8GCSqGSIb3DQEJBDEi
# BCCYRKetUGAWOrQ1Ez1ADEOWeznotp2wiojAnsZwuIJYFzANBgkqhkiG9w0BAQEF
# AASCAgA/LYOoXE2pkVlQmnLgfd0glD8owDF8PDpxZhusSTxLhwXaqJ7I9hKbjxK7
# Eb3JqH/LuLYrLJU9k6ivGs3+96homecw0+hu6Z88Bcy+ffAJ0uUgwLZYfgCN+yU7
# 56cy+3favlex8KP/ckhGNj7HCbFoAvpqf8geHaYlpwfr6ONySLmlVx2LWAvvZNjd
# K9SrkIIOzDgSOBC+Zrl6a/qR+v9eBsLKVGAFJ6DQWxFekFJyV3TkxCGJLadEcCur
# BnA6h9Ncugerl16HDAiUchTbC0JqO0DVg2DHKIxAzQaIuTiMF+SVfKN70COBIZJq
# mAvv8O550mRZ1J5Jn3KpQCpPGqiCKrlxCHiUhA78DeN4PXoDWKvpIrOesgQN7JFI
# u4bG1nFW6SEGvy6GRl88uGgtxfcqGF4UPbwlBRs+vzguMG3e874sOlxuhYZ5vnPb
# BHCdDMbChrM7Dx9nRz/WPOZReG/gdX1YhQcfpNuvUSdQA8Cpos7pqRFf5vbNTnjp
# vEs1puJQS0FhzBHbK2rMYrVpDWYp0IH80y2CdrAwAczSKJ2U8C9RCcOKNGx57QNI
# VS5d11iQRbTslZLw77mldJAVKiGSNcaCxILhmp9/83FMBqqKrK6EnOnB+TfKenfm
# UfOHLgAjh/pTf2hpmAZpTBMcWK13wdFMfz9/ELOh9t3mQ3o2Tw==
# SIG # End signature block