src/MailPolicyExplainer.psm1

<#
MailPolicyExplainer.psm1 -- source file for said module
Copyright (C) 2018, 2020, 2023-2024 Colin Cogle. All Rights Reserved.
 
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
 
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
 
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
#>


#region Helper functions
# The following functions are used internally by MailPolicyExplainer and are not
# exposed to the end user.

Function Write-GoodNews
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification='Output colored text in a PS5-compatible manner.')]
    [OutputType([Void])]
    Param(
        [Parameter(Position=0)]
        [AllowNull()]
        [String] $Message
    )

    Write-Host -ForegroundColor Green -Object "✅`t$Message"
}

Function Write-BadPractice
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification='Output colored text in a PS5-compatible manner.')]
    [OutputType([Void])]
    Param(
        [Parameter(Position=0)]
        [AllowNull()]
        [String] $Message
    )

    Write-Host -ForegroundColor Yellow -Object "🟨`t$Message"
}

Function Write-BadNews
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification='Output colored text in a PS5-compatible manner.')]
    [OutputType([Void])]
    Param(
        [Parameter(Position=0)]
        [AllowNull()]
        [String] $Message
    )

    Write-Host -ForegroundColor Red -Object "❌`t$Message"
}

Function Write-Informational
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification='Output colored text in a PS5-compatible manner.')]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [AllowNull()]
        [String] $Message
    )

    Write-Host -ForegroundColor White -Object "ℹī¸`t$Message"
}

Function Write-DnsLookups
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='We are counting multiple lookups.')]
    [OutputType([String])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [UInt32] $DnsLookups,

        [Switch] $Enabled
    )

    If ($Enabled) {
        Return " ($DnsLookups/10 DNS lookups)"
    }
}

Function Get-RandomString
{
    [OutputType([String])]
    Param()

    # We're going to return a random string of varying length to prevent passive
    # cryptanalysis attacks (which are extremely unlikely). 16 to 256 bytes of
    # added entropy should be sufficient, without pushing our packets too close
    # to the smallest-possible MTU of 576 bytes (for IPv4).
    $retvalLength = Get-Random -Minimum 16 -Maximum 256

    # Per Google's advice, we will use random padding consisting of URL-safe
    # characters: A-Z, a-z, 0-9, period, underscore, hyphen, and tilde.
    # Because Get-Random removes an item after selecting it, we're "multiplying"
    # this string array by 30, so that we can pull up to $(2048 - 90) characters
    # in Invoke-GooglePublicDnsApi (in case someone decides to increase the
    # -Maximum value to the previous Get-Random call).
    $chars = [Char[]]('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~._-' * 30)
    Return ((Get-Random -InputObject $chars -Count $retvalLength) -Join '')
}

Function Get-RSAPublicKeyLength
{
    [OutputType([UInt16])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [String] $PublicKey
    )

    $rsa = [Security.Cryptography.RSACryptoServiceProvider]::new()

    # .NET 7 adds the ImportFromPem method to instances of the RSA class.
    # If it's available, use it.
    If ($null -ne (Get-Member -InputObject $rsa | Where-Object Name -eq 'ImportFromPem'))
    {
        $rsa.ImportFromPem("-----BEGIN PUBLIC KEY-----`r`n$PublicKey`r`n-----END PUBLIC KEY-----")
        Return $rsa.KeySize
    }
    # If we're using the older .NET Framework (Windows PowerShell), then we can
    # only guess on the key length by looking at the size of the encoded data.
    # If anyone knows a better way to make this work on .NET 6 and older, please
    # submit a pull request!
    Else {
        Write-Verbose 'Accurate DKIM key length detection requires PowerShell 7. We will do our best to guess.'
        Switch ($PublicKey.Length) {
            392        {Return 2048}
            216        {Return 1024}
            168        {Return 768}
            128        {Return 512}
            default    {Return 'unknown'}
        }
    }
}

Function Test-IPv4Address
{
    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String] $HostName
    )

    Return (Invoke-GooglePublicDnsApi $HostName -Type 'A').PSObject.Properties.Name -Match 'Answer'
}

Function Test-IPv6Address
{
    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String] $HostName
    )

    Return (Invoke-GooglePublicDnsApi $HostName -Type 'AAAA').PSObject.Properties.Name -Match 'Answer'
}
#endregion Helper functions

Function Invoke-GooglePublicDnsApi
{
    [CmdletBinding()]
    [OutputType([PSObject])]
    Param(
        [Parameter(Position=0, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String] $InputObject,

        [Parameter(Position=1)]
        [ValidateSet('A', 'AAAA', 'CNAME', 'MX', 'SPF', 'TLSA', 'TXT')]
        [String] $Type = 'A',

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $MaxLengthOfPadding = 1958 - $InputObject.Length - $Type.Length

    If ($DisableDnssecVerification) {
        $CD = 1
    } Else {
        $CD = 0
    }

    $ToSend = @{
        'name'           = $InputObject
        'type'           = $Type
        'ct'             = 'application/x-javascript'
        'cd'             = $CD    # enable DNSSEC validation (by default)...
        'do'             = 0    # ...but don't return RRSIGs. Trust the resolver.
        'random_padding' = Get-RandomString -MaxLength $MaxLengthOfPadding -MinLength $MaxLengthOfPadding
    }

    Write-Verbose "Sending $($ToSend.random_padding.Length) characters of random padding."

    # DNS-over-HTTPS requests are supposed to use HTTP/2 or newer. However,
    # Invoke-RestMethod's -HttpVersion parameter was added in PowerShell 7.3.
    # Downlevel versions of PowerShell only used HTTP/1.1, which is thankfully
    # supported by the Google Public DNS API.
    #
    # Thus, our code will attempt to use HTTP/3 if it's available, and fall back
    # to the system default if not.
    $RequestParams = @{
        'Method'  = 'GET'
        'Uri'     = 'https://dns.google/resolve'
        'Body'    = $ToSend
        'Verbose' = $VerbosePreference
    }
    If ((Get-Command 'Invoke-RestMethod').Parameters.Keys -Contains 'HttpVersion') {
        $RequestParams += @{'HttpVersion' = '3.0'}
    }

    $result = Invoke-RestMethod @RequestParams
    Write-Debug $result
    Return $result
}

Function Test-IPVersions
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='We are always testing both IP versions.')]
    [CmdletBinding()]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String] $HostName,

        [Parameter(DontShow)]
        [Switch] $IndentOutput
    )

    $Indent = ''
    If ($IndentOutput) {
        $Indent = '├──'
    }

    If (Test-IPv4Address $HostName) {
        Write-GoodNews "${Indent}IP: The server $HostName has an IPv4 address."
    }
    Else {
        Write-BadPractice "${Indent}IP: The server $HostName has no IPv4 addresses. IPv4-only clients cannot reach this server."
    }

    If (Test-IPv6Address $HostName) {
        Write-GoodNews "${Indent}IP: The server $HostName has an IPv6 address."
    }
    Else {
        Write-BadPractice "${Indent}IP: The server $HostName has no IPv6 addresses. IPv6-only clients cannot reach this server!"
    }
}

Function Test-AdspRecord
{
    [CmdletBinding()]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string] $DomainName,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "_adsp._domainkey.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification
    $ADSPRecordFound = $DnsLookup.PSObject.Properties.Name -Contains 'Answer' -and $DnsLookup.Status -ne 3

    #region DNSSEC check
    # Since DKIM ADSP is historic, I don't want the DNSSEC-authenticated denial
    # of existence to show up when using Test-MailPolicy. Only show the DNSSEC
    # information when calling this function directly, or if there is an ADSP
    # record to display.
    If (-Not $DisableDnssecVerification -and ($ADSPRecordFound -or ((Get-PSCallStack).Command)[1] -ne 'Test-MailPolicy'))
    {
        If ($DnsLookup.AD) {
            Write-GoodNews "DKIM ADSP: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "DKIM ADSP: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If (-Not $ADSPRecordFound)
    {
        Write-Verbose 'DKIM ADSP: No ADSP record was found.'
    }
    Else
    {
        Write-BadPractice "DKIM ADSP: Author Domain Signing Practices is declared historic and should not be relied on."
        $AdspRecord = $DnsLookup.Answer.Data

        If ($AdspRecord -Eq "dkim=unknown") {
            Write-Informational "DKIM ADSP: This domain's ADSP is unknown; it may sign no, some, most, or all email with DKIM."
        }
        ElseIf ($AdspRecord -Eq "dkim=all") {
            Write-GoodNews "DKIM ADSP: ADSP says all email from this domain will have a DKIM signature."
        }
        ElseIf ($AdspRecord -Eq "dkim=discardable") {
            Write-GoodNews "DKIM ADSP: ADSP says all email from this domain will have a DKIM signature, and mail with a missing or bad signature should be discarded."
        }
        Else {
            Write-BadNews "DKIM ADSP: An invalid ADS practice was specified ($AdspRecord)."
        }
    }
}

Function Test-BimiSelector
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-BimiRecord')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string] $DomainName,

        [Parameter(Position=1)]
        [Alias('Selector', 'SelectorName')]
        [string] $Name = 'default',

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "$Name._bimi.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "BIMI selector ${Selector}: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "BIMI selector ${Selector}: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-Informational "BIMI selector ${Selector}: Not found!"
        Return
    }

    $BimiRecord = ($DnsLookup.Answer | Where-Object type -eq 16).Data
    If ($null -eq $BimiRecord)
    {
        Write-BadNews "BIMI selector ${Selector}: A record exists with no valid data!"
        Return
    }

    ForEach ($token in ($BimiRecord -Split ';')) {
        $token = $token.Trim()

        If ($token -Eq "v=BIMI1") {
            Write-GoodNews "BIMI selector ${Selector}: This is a BIMI version 1 record."
        }

        # BIMI evidence document tag
        ElseIf ($token -Like "a=*") {
            $policy = $token -Replace 'a='
            If ($null -ne $policy) {
                Write-GoodNews "BIMI selector ${Selector}: An authority evidence document can be found at $policy."
            }
            Else {
                Write-Informational 'BIMI selector ${Selector}: No authority evidence is available.'
            }
        }
        ElseIf ($token -Like "l=*") {
            $locationURI = $token -Replace 'l='
            If ($null -eq $locationURI) {
                Write-Informational "BIMI selector ${Selector}: This domain does not participate in BIMI."
            }
            ElseIf ($locationURI -Like 'https://*') {
                Write-GoodNews "BIMI selector ${Selector}: The brand indicator is at $locationURI."
            }
            Else {
                Write-BadNews "BIMI selector ${Selector}: The brand indicator must be available over HTTPS! ($locationURI)"
            }
        }
        ElseIf ($token.Length -gt 0) {
            Write-BadNews "BIMI selector ${Selector}: An invalid tag was specified ($token)."
        }
    }
}

Function Test-DaneRecord
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-DaneRecords', 'Test-TlsaRecord', 'Test-TlsaRecords')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String] $DomainName,

        [Parameter(DontShow)]
        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    # Fetch all MX records for this domain.
    $MXServers = @()
    Invoke-GooglePublicDnsApi $DomainName 'MX' -Debug:$DebugPreference `
        | Select-Object -ExpandProperty Answer `
        | Where-Object type -eq 15 `
        | Select-Object -ExpandProperty Data `
        | ForEach-Object `
    {
        $Preference, $Name = $_ -Split "\s+"
        $MXServers += @{'Preference'=[UInt16]$Preference; 'Server'=$Name}
    }

    If ($MXServers.Count -eq 1 -and $MXServers[0].Server -eq '.') {
        Write-Verbose 'DANE: This domain does not receive email.'
        Return
    }

    # Check for the confusing case where a domain has no MX servers, and does
    # not publish a null MX record. In that case, the domain's A and AAAA records
    # will be substituted as a mail exchanger with preference 0. (Really, that's
    # what it says to do in the RFC. Go look it up.)
    #
    # We're checking for a count of zero, or a count of one where the server
    # name is blank, just in case I add options for other DNS APIs in the future.
    # Google Public DNS's API returns the latter format.
    If ($MXServers.Count -eq 0)
    {
        $MXServers = @(@{'Preference'=0; 'Server'=$DomainName})
    }

    $MXServers | Sort-Object Preference | ForEach-Object {
        # Strip the trailing dot, if present. This is done for display purposes.
        $MXName = $_.Server -Replace '\.$'

        $DnsLookup = Invoke-GooglePublicDnsApi "_25._tcp.$MXName" 'TLSA' -Debug:$DebugPreference
        $FoundDANERecords = ($DnsLookup.PSObject.Properties.Name -Contains 'Answer') -and ($DnsLookup.Status -ne 2) -and ($DnsLookup.Status -ne 3)

        #region DNSSEC check
        # Complain if the user attempted to disable DNSSEC checking. That's a
        # requirement for DANE. Politely refuse to honor the user's request and
        # check DNSSEC anyway. This will only happen if the user is entering
        # this function call via Test-MailPolicy.
        If ($FoundDANERecords)
        {
            If ($DisableDnssecVerification -and -not $ShowedDnssecWarning)
            {
                Write-Informational 'DANE: Records must be signed with DNSSEC. Validating DNSSEC anyway.'
                $ShowedDnssecWarning = $true
            }

            If ($DnsLookup.AD) {
                Write-GoodNews "DANE: ${MXName}: The DNS lookup is secure."
            }
            Else {
                Write-BadNews "DANE: ${MXName}: The DNS lookup is insecure; the DANE records cannot be used! Enable DNSSEC for this domain."
                Return
            }
        }
        #endregion

        If (-Not $FoundDANERecords)
        {
            Write-BadNews "DANE: DANE records are not present for ${MXName}, TCP port 25."
            Return
        }

        ($DnsLookup.Answer | Where-Object type -eq 52).Data | ForEach-Object {
            $Usage, $Selector, $Type, $CertData = $_ -Split '\s+'

            If ($Selector -NotIn 0,1) {
                Write-BadNews "DANE: ${MXName}: The DANE record is invalid! (Unknown Selector $Selector)"
                Continue
            }
            ElseIf ($Type -NotIn 0,1,2) {
                Write-BadNews "DANE: ${MXName}: The DANE record is invalid! (Unknown Type $Selector)"
                Continue
            }

            Switch ($Usage) {
                0 {
                    Write-BadPractice "DANE: ${MXName}: Found a PKIX-TA record, which is not supported for SMTP: $Usage $Selector $Type $CertData (not checked)"
                }
                1 {
                    Write-BadPractice "DANE: ${MXName}: Found a PKIX-EE record, which is not supported for SMTP: $Usage $Selector $Type $CertData (not checked)"
                }
                2 {
                    Write-GoodNews "DANE: ${MXName}: Found a DANE-TA record: $Usage $Selector $Type $CertData (not checked)"
                }
                3 {
                    Write-GoodNews "DANE: ${MXName}: Found a DANE-EE record: $Usage $Selector $Type $CertData (not checked)"
                }
                default {
                    Write-BadNews "DANE: ${MXName}: The DANE record is invalid! (Unknown Usage $Usage)"
                }
            }
        }
    }
}

Function Test-DkimSelector
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-DkimRecord', 'Test-DomainKeysSelector', 'Test-DomainKeysRecord')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string] $DomainName,

        [Parameter(Mandatory, Position=1)]
        [Alias('Selector', 'SelectorName', 'KeyName')]
        [string]$Name,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "$Name._domainkey.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification
    $Name = " $Name"

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "DKIM selector${Name}: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "DKIM selector${Name}: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadNews "DKIM selector${Name}: This selector was not found."
        Return
    }

    $DkimKeyRecord = ($DnsLookup.Answer | Where-Object type -eq 16).Data
    If ($null -eq $DkimKeyRecord)
    {
        Write-BadNews "DKIM selector${Name}: This selector was not found in DNS."
        Return
    }
    Else {
        Write-Verbose "DKIM selector${Name}: `"$DkimKeyRecord`""
    }

    #region Check for default values.
    # If there is no "k=" token, it's assumed to be "k=rsa" (per the RFC).
    # Additionally, if there is no "v=" token, it's assumed to be "v=DKIM1".
    $VersionImplied = $false
    $KeyTypeImplied = $false

    If ($DkimKeyRecord -NotLike "*v=*")
    {
        $DkimKeyRecord = "v=DKIM1; $DkimKeyRecord"
        $VersionImplied = $true
    }
    If ($DkimKeyRecord -NotLike "*k=*")
    {
        $DkimKeyRecord = $DkimKeyRecord.Replace(';', ';k=rsa;', 1)
        $KeyTypeImplied = $true
    }
    #endregion

    ForEach ($token in ($DkimKeyRecord -Split ';')) {
        $token = $token.Trim()
        If ($token -Like "v=*") {
            $version = $token -Replace 'v=',''
            If ($VersionImplied) {
                Write-GoodNews "DKIM selector${Name}: This is implied to conform to DKIM version 1."
            }
            ElseIf ($version -Eq 'DKIM1') {
                Write-GoodNews "DKIM selector${Name}: This conforms to DKIM version 1."
            } Else {
                Write-BadNews "DKIM selector${Name}: This does not conform to DKIM version 1."
                Return
            }
        }
        ElseIf ($token -Like "s=*") {
            ForEach ($purpose in ($token -Replace 's=' -Split ':')) {
                $purpose = $purpose.Trim()
                If ($purpose -Eq '*') {
                    Write-GoodNews "DKIM selector${Name}: This key is valid for all purposes."
                }
                ElseIf ($purpose -Eq 'email') {
                    Write-GoodNews "DKIM selector${Name}: This key is valid for email."
                }
                Else {
                    Write-BadPractice "DKIM selector${Name}: This key is valid for $purpose, which is not part of the DKIM specification."
                }
            }
        }
        ElseIf ($token -Like "k=*") {
            $algorithm  = $token -Replace 'k='

            If ($KeyTypeImplied) {
                Write-GoodNews "DKIM selector${Name}: This is implied to have an RSA key."
            }
            ElseIf ($algorithm -Eq 'rsa') {
                Write-GoodNews "DKIM selector${Name}: This has an RSA key."
            }
            ElseIf ($algorithm -eq 'ed25519') {
                Write-GoodNews "DKIM selector${Name}: This has an Ed25519 key. Not all verifiers can verify these newer keys."
            }
            Else {
                Write-BadNews "DKIM selector${Name}: This has an unknown key type ($algorithm)!"
            }
        }
        ElseIf ($token -Like "h=*") {
            ForEach ($algorithm in ($token -Replace 'h=' -Split ':')) {
                $algorithm = $algorithm.Trim()
                If ($algorithm -Eq 'sha1') {
                    Write-BadPractice "DKIM selector${Name}: This key will sign only SHA-1 hashes, which are deprecated."
                }
                ElseIf ($algorithm -Eq 'sha256') {
                    Write-GoodNews "DKIM selector${Name}: This key will sign only SHA-256 hashes."
                }
                Else {
                    Write-BadNews "DKIM selector${Name}: This key will sign only $algorithm hashes, which are not part of the DKIM specification."
                }
            }
        }
        ElseIf ($token -Like 't=*') {
            ForEach ($flag in ($token -Replace 't=' -Split ':')) {
                $flag = $flag.Trim()
                If ($flag -Eq 'y') {
                    Write-Informational "DKIM selector${Name}: This domain is testing DKIM; recipients should treat signed and unsigned messages identically."
                }
                ElseIf ($flag -Eq 's') {
                    Write-GoodNews "DKIM selector${Name}: This selector is not valid for subdomains."
                }
                Else {
                    Write-BadNews "DKIM selector${Name}: An unknown flag $flag was specified."
                }
            }
        }
        ElseIf ($token -Like "g=*") {
            $username = $token -Replace 'g='
            Write-Informational "DKIM selector${Name}: This selector will only sign emails from the username $username."
        }
        ElseIf ($token -Like 'p=*') {
            $publickey = $token -Replace 'p='

            If ($DkimKeyRecord -match 'k=ed25519') {
                Write-GoodNews "DKIM selector${Name}: The Ed25519 public key size is 256 bits."
            }
            ElseIf ($DkimKeyRecord -Match 'k=rsa') {
                $bits = Get-RSAPublicKeyLength $publickey
                If ($bits -gt 4096) {
                    Write-BadPractice "DKIM selector${Name}: The RSA public key size is $bits bits. Verifiers may not support keys this large."
                }
                ElseIf ($bits -ge 2048) {
                    Write-GoodNews "DKIM selector${Name}: The RSA public key size is $bits bits."
                }
                ElseIf ($bits -ge 1024) {
                    Write-BadPractice "DKIM selector${Name}: The RSA public key size is only $bits bits. Upgrade to 2048 bits."
                }
                Else {
                    Write-BadNews "DKIM selector${Name}: The RSA public key size is only $bits bits. This key is too small to be used. Replace it with an Ed25519 or 2048-bit RSA key!"
                }
            }
        }
        ElseIf ($token -Like 'n=*') {
            Write-Informational "DKIM selector${Name}: Some notes: $($token -Replace 'n=')"
        }
        ElseIf ($token.Length -gt 0) {
            Write-BadNews "DKIM selector${Name}: An invalid selector token was specified ($token)."
        }
    }
}

Function Test-DmarcRecord
{
    [CmdletBinding()]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string] $DomainName,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "_dmarc.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "DMARC: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "DMARC: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadNews "DMARC: Not found!"
        Return
    }

    $DmarcRecord = ($DnsLookup.Answer | Where-Object type -eq 16).Data
    If ($null -eq $DmarcRecord)
    {
        Write-BadNews "DMARC: A record exists with no valid data!"
        Return
    }

    ForEach ($token in ($DmarcRecord -Split ';')) {
        $token = $token.Trim()

        If ($token -Eq "v=DMARC1") {
            Write-GoodNews "DMARC: This is a DMARC version 1 record."
        }
        ElseIf ($token -Like "p=*") {
            $policy = $token -Replace 'p='
            If ($policy -Eq 'none') {
                Write-Informational 'DMARC: Report but deliver messages that fail DMARC.'
            } ElseIf ($policy -Eq 'quarantine') {
                Write-GoodNews 'DMARC: Quarantine messages that fail DMARC.'
            } ElseIf ($policy -Eq 'reject') {
                Write-GoodNews 'DMARC: Reject messages that fail DMARC.'
            } Else {
                Write-BadNews "DMARC: An invalid policy was specified ($policy)."
            }
        }
        ElseIf ($token -Like "sp=*") {
            $subdomainpolicy = $token -Replace 'sp='
            If ($subdomainpolicy -Eq 'none') {
                Write-Informational 'DMARC: Report but deliver messages from subdomains (without their own DMARC records) that fail DMARC.'
            } ElseIf ($subdomainpolicy -Eq 'quarantine') {
                Write-GoodNews 'DMARC: Quarantine messages from subdomains (without their own DMARC records) that fail DMARC.'
            } ElseIf ($subdomainpolicy -Eq 'reject') {
                Write-GoodNews 'DMARC: Reject messages from subdomains (without their own DMARC records) that fail DMARC.'
            } Else {
                Write-BadNews "DMARC: An invalid subdomain policy was specified ($subdomainpolicy)."
            }
        }
        ElseIf ($token -Like "pct=*") {
            $pct = [Byte]($token -Replace 'pct=')
            If ($pct -eq 100) {
                If ($policy -Match "reject") {
                    Write-Informational "DMARC: Reject 100% of email that fails DMARC (default)."
                }
                ElseIf ($policy -Match 'quarantine') {
                    Write-Informational "DMARC: Quarantine 100% of email that fails DMARC (default)."
                }
            }
            Else {
                If ($policy -Match "reject") {
                    Write-Informational "DMARC: Only reject ${pct}% of unaligned email; the rest will be quarantined."
                }
                ElseIf ($policy -Match 'quarantine') {
                    Write-BadPractice "DMARC: Only quarantine ${pct}% of unaligned email; the rest will be delivered."
                }
            }
        }
        ElseIf ($token -Like "aspf=*") {
            Switch ($token -Replace 'aspf=') {
                's'  { Write-Informational 'DMARC: SPF alignment is strict (From domain = MailFrom domain).' }
                'r'  { Write-Informational 'DMARC: SPF alignment is relaxed (From domain = MailFrom domain or a subdomain; default).' }
                Else { Write-BadNews  "DMARC: An invalid SPF alignment was specified ($token)." }
            }
        }
        ElseIf ($token -Like "adkim=*") {
            Switch ($token -Replace 'adkim=') {
                's'  { Write-Informational 'DMARC: DKIM alignment is strict (domain = signing domain).' }
                'r'  { Write-Informational 'DMARC: DKIM alignment is relaxed (domain = signing domain or a subdomain; default).' }
                Else { Write-BadNews  "DMARC: An invalid DKIM alignment was specified ($token)." }
            }
        }
        ElseIf ($token -Like 'fo=*') {
            If ($DmarcRecord -Match 'ruf=') {
                Switch ($token.Substring(3) -Split ':') {
                    0    { Write-Informational 'DMARC: Generate a forensic report if SPF and DKIM both fail (default).' }
                    1    { Write-Informational 'DMARC: Generate a forensic report if either SPF or DKIM fail.'}
                    'd'  { Write-Informational 'DMARC: Generate a forensic report if DKIM fails, even if DMARC passes.' }
                    's'  { Write-Informational 'DMARC: Generate a forensic report if SPF fails, even if DMARC passes.' }
                    Else { Write-BadNews  "DMARC: An invalid failure reporting tag was specified ($token)." }
                }
            } Else {
                Write-BadPractice 'DMARC: The failure reporting options will be ignored because a forensic report destination (ruf) was not specified.'
            }
        }
        ElseIf ($token -Like 'rf=*') {
            $formats = $token.Substring(3) -Split ':'
            ForEach ($format in $formats) {
                $format = $format.Trim()
                If ($format -eq 'afrf') {
                    Write-Informational 'DMARC: Failure reports can be sent in AFRF format (default).'
                }
                Else {
                    Write-BadNews "DMARC: The reporting format $format is not an allowed format. Mail receivers may ignore the entire DMARC record."
                }
            }
        }
        ElseIf ($token -Like 'ri=*') {
            $interval = [UInt32]($token -Replace 'ri=')
            If ($interval -Eq 86400) {
                Write-Informational "DMARC: Aggregate reports should (if possible) be sent no more than daily (default)."
            } Else {
                Write-Informational "DMARC: Aggregate reports should (if possible) be sent no more than every $interval seconds."
            }
        }
        ElseIf ($token -Like 'rua=*') {
            ForEach ($destination in ($token -Replace 'rua=' -Split ',')) {
                Write-Informational "DMARC: Aggregate reports will be sent to $destination."
            }
        }
        ElseIf ($token -Like 'ruf=*') {
            ForEach ($destination in ($token -Replace 'ruf=' -Split ',')) {
                Write-Informational "DMARC: Forensic reports will be sent to $destination (if the sender supports forensic reports)."
            }
        }
        ElseIf ($token.Length -gt 0) {
            Write-BadNews "DMARC: An invalid tag was specified ($token)."
        }
    }
}

Function Test-MailPolicy
{
    [CmdletBinding()]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [String] $DomainName,

        [Alias('Recurse')]
        [Switch] $CountSpfDnsLookups,

        [String[]] $DkimSelectorsToCheck,

        [String[]] $BimiSelectorsToCheck,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    Write-Output "Analyzing email records for $DomainName"
    Test-MXRecord $DomainName -DisableDnssecVerification:$DisableDnssecVerification
    Test-SpfRecord $DomainName -Recurse:$CountSpfDnsLookups -DisableDnssecVerification:$DisableDnssecVerification
    If ($DkimSelectorsToCheck.Count -gt 0) {
        $DkimSelectorsToCheck | ForEach-Object {
            Test-DkimSelector $DomainName -Name $_ -DisableDnssecVerification:$DisableDnssecVerification
        }
    }
    Test-ADSPRecord $DomainName -DisableDnssecVerification:$DisableDnssecVerification
    Test-DmarcRecord $DomainName -DisableDnssecVerification:$DisableDnssecVerification
    If ($BimiSelectorsToCheck.Count -gt 0) {
        $BimiSelectorsToCheck | ForEach-Object {
            Test-BimiSelector $DomainName -Name $_ -DisableDnssecVerification:$DisableDnssecVerification
        }
    }
    Test-MtaStsPolicy $DomainName -DisableDnssecVerification:$DisableDnssecVerification
    Test-SmtpTlsReportingPolicy $DomainName -DisableDnssecVerification:$DisableDnssecVerification
    Test-DaneRecord $DomainName -DisableDnssecVerification:$DisableDnssecVerification
}

Function Test-MtaStsPolicy
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-MtaStsRecord')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [String] $DomainName,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "_mta-sts.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "MTA-STS Record: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "MTA-STS Record: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadNews "MTA-STS Record: Not found! (Skipping policy test.)"
        Return
    }

    $MtaStsRecord = ($DnsLookup.Answer | Where-Object Type -eq 16).Data
    If ($null -eq $MtaStsRecord)
    {
        Write-BadNews "MTA-STS Record: A record exists with no valid data!"
        Return
    }

    $validSTSrecords = 0
    ForEach ($token in ($MtaStsRecord -Split ';')) {
        $token = $token.Trim()

        If ($token -CLike "v=*") {
            If ($token -eq 'v=STSv1') {
                Write-GoodNews "MTA-STS Record: This domain's STS record is version 1."
                $validSTSrecords++
            }
            Else {
                Write-BadNews "MTA-STS Record: This domain's STS record is an unsupported version ($($token -Replace 'v='))."
            }
        }
        ElseIf ($token -CLike 'id=*') {
            Write-Informational "MTA-STS Record: The domain's policy tag is $($token -Replace 'id=')."
        }
        ElseIf ($token.Length -gt 0) {
            Write-BadNews "MTA-STS Record: An unknown tag was found: $token"
        }
    }
    If ($validSTSrecords -ne 1) {
        Write-BadNews "MTA-STS Record: We did not find exactly one STS TXT record. We must assume MTA-STS is not supported!"
        Return
    }

    #region Fetch the MTA-STS policy file.
    # Connect to the remote server and download the file. We'll try with TLS 1.3
    # first, then again with TLS 1.2. (TLS version support depends on the host
    # operating system and PowerShell version.)
    Test-IPVersions "mta-sts.$DomainName"

    $oldSP      = [Net.ServicePointManager]::SecurityProtocol
    $ModuleInfo = (Get-Module 'MailPolicyExplainer')
    $iwrParams  = @{
        'Method'          = 'GET'
        'Uri'             = "https://mta-sts.$DomainName/.well-known/mta-sts.txt"
        'UseBasicParsing' = $true
        'UserAgent'       = "Mozilla/5.0 ($($PSVersionTable.Platform); $($PSVersionTable.OS); $PSCulture) PowerShell/$($PSVersionTable.PSVersion) MailPolicyExplainer/$($ModuleInfo.Version)"
        'ErrorAction'     = 'Stop'
    }
    Try {
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls13
        $policy = Invoke-WebRequest @iwrParams
        Write-GoodNews "MTA-STS Policy: Downloaded the policy file from mta-sts.$DomainName using TLS 1.3."
    }
    Catch {
        Try {
            [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
            $policy = Invoke-WebRequest @iwrParams
            Write-GoodNews "MTA-STS Policy: Downloaded the policy file from mta-sts.$DomainName using TLS 1.2."
        }
        Catch {
            Write-BadNews "MTA-STS Policy: Could not connect to mta-sts.$DomainName using TLS 1.2 or 1.3. Older TLS versions are not permitted."
            Return
        }
    }
    [Net.ServicePointManager]::SecurityProtocol = $oldSP
    #endregion

    #region Parse the downloaded file.
    # It must be a text/plain document.
    If (-Not ($policy.Headers.'Content-Type' -Match "^text/plain(;.*)?$")) {
        Write-BadNews "MTA-STS Policy: It was found, but was returned with the wrong content type ($($policy.Headers.'Content-Type'))."
    }
    Else {
        #region Make sure the file has the correct line endings.
        # The MTA-STS RFC says that they should end with CRLF (i.e., "`r`n").
        # Split it up two different ways and see if we get the same results.
        # If not, then someone probably saved the file with UNIX ("`r") endings.
        # We're going to be strict and refuse to parse the file in this case.
        $lines   = $policy.Content -Split "`r`n"
        $LFlines = $policy.Content -Split "`n"

        If ($lines.Count -ne $LFLines.Count) {
            Write-Debug "This file has $($lines.Count) CRLF-terminated lines and $($LFlines.Count) LF-terminated lines."
            Write-BadNews "MTA-STS Policy: The policy file does not have the correct CRLF line endings!"
            Return
        }
        #endregion

        $lines | ForEach-Object {
            $line = $_.Trim()
            If ($line -CLike 'version: *') {
                If (($line -Split ':')[1].Trim() -Eq 'STSv1') {
                    Write-GoodNews "MTA-STS Policy: This domain's STS policy is version 1."
                }
                Else {
                    Write-BadNews "MTA-STS Policy: This domain's STS policy has an undefined version ($line)."
                }
            }
            ElseIf ($line -CLike 'mode: *') {
                $mode = ($line -Split ':')[1].Trim()
                If ($mode -Eq 'enforce') {
                    Write-GoodNews 'MTA-STS Policy: This domain enforces MTA-STS. Senders must not deliver mail to hosts that do not offer STARTTLS with a valid, trusted certificate.'
                } ElseIf ($mode -Eq 'testing') {
                    Write-Informational 'MTA-STS Policy: This domain is in testing mode. MTA-STS failures will be reported, but the message will be delivered.'
                } ElseIf ($mode -Eq 'none') {
                    Write-BadPractice 'MTA-STS Policy: This domain has no active policy.'
                } Else {
                    Write-BadNews "MTA-STS Policy: The unknown mode $mode was specified."
                }
            } ElseIf ($line -CLike 'mx: *') {
                If ($line -Match '\*') {
                    Write-GoodNews "MTA-STS Policy: This domain has MX hosts with STARTTLS and valid certificates matching $((($line -Split ':')[1]).Trim())."
                }
                Else {
                    Write-GoodNews "MTA-STS Policy: The domain has an MX host with STARTTLS and a valid certificate at $((($line -Split ':')[1]).Trim())."
                }
            }
            ElseIf ($line -CLike 'max_age: *') {
                # RFC 8461 doesn't define a data type for max_age, only saying that it is a "plaintext non-negative
                # integer seconds" with a maximum of 31557600. The smallest type that can hold that is UInt32.
                $seconds = [UInt32]$(($line -Split ':')[1].Trim())
                If ($seconds -gt 31557600) {
                    Write-BadPractice "MTA-STS Policy: This policy should be cached for $seconds seconds, which is longer than the maximum of 31557600 seconds."
                }
                Else {
                    Write-Informational "MTA-STS Policy: This policy should be cached for $seconds seconds."
                }
            }
            ElseIf ($line.Length -gt 0) {
                Write-BadNews "MTA-STS Policy: An unknown key/value pair was specified: $line"
            }
        }
    }
    #endregion
}

Function Test-MXRecord
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-MXRecords', 'Test-NullMXRecord')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [String] $DomainName,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $Results   = @()
    $DnsLookup = Invoke-GooglePublicDnsApi $DomainName 'MX' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "MX: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "MX: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion DNSSEC check

    #region Implied MX record check
    # Check to see if we should create an implied MX record from the root A/AAAA
    # records, or if there are proper MX records alread in place.
    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadPractice "MX: There are no MX records! This implies the domain will receive its own email."
        $Results += @{"Preference"=0; "Server"=$DomainName; "Implied"=$true}
    }
    ElseIf ($DnsLookup.Status -eq 0)
    {
        ($DnsLookup.Answer | Where-Object Type -eq 15).Data | ForEach-Object {
            $Pref, $Server = $_ -Split "\s+"
            $Results += @{"Preference"=[UInt16]$Pref; "Server"=$Server; "Implied"=$false}
        }
    }
    Else {
        Write-Error "MX: DNS lookup failed with status $($DnsLookup.Status)."
    }
    #endregion

    #region Null MX check
    If ($Results.Count -eq 1 -and $Results[0].Server -eq '.') {
        Write-Informational 'MX: This domain does not send or receive email.'
        Return
    }
    #endregion

    $Results | Sort-Object Preference | ForEach-Object {
        If ($_.Implied) {
            Write-GoodNews "MX: This domain is its own MX server."
        }
        Else {
            Write-GoodNews "MX: The server $($_.Server) can receive mail for this domain (at priority $($_.Preference))."
        }
        Test-IPVersions ($_.Server) -Indent
    }
}

Function Test-SmtpTlsReportingPolicy
{
    [CmdletBinding()]
    [OutputType([Void])]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string] $DomainName,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification
    )

    $DnsLookup = Invoke-GooglePublicDnsApi "_smtp._tls.$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "TLSRPT: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "TLSRPT: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion DNSSEC check
    
    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadNews "TLSRPT: SMTP TLS Reporting is not enabled for this domain."
        Return
    }
    ElseIf (($DnsLookup.Answer | Where-Object type -eq 16).Count -gt 1) {
        Write-BadNews "TLSRPT: More than one DNS record was found. SMTP TLS Reporting must be assumed not to be supported!"
        Return
    }

    $TlsRptPolicy = ($DnsLookup.Answer | Where-Object type -eq 16).Data
    If ($null -eq $TlsRptPolicy)
    {
        Write-Verbose "TLSRPT: A policy record exists with no valid data!"
        Return
    }

    # The "rua" tag must appear at least once, and the "v" tag must appear
    # exactly once. We'll count how many times we see each one.
    $ruas     = 0
    $versions = 0

    ForEach ($token in ($TlsRptPolicy -Split ';'))
    {
        $splits = $token -Split '='
        $key    = $splits[0].Trim()
        $value  = ''
        If ($null -ne $splits[1]) {
            $value = $splits[1].Trim()
        }

        If ($key -eq 'v')
        {
            If ($value -eq 'TLSRPTv1') {
                Write-GoodNews 'TLSRPT: This is a version 1 policy.'
                $versions++
            } Else {
                Write-BadNews "TLSRPT: This is an unsupported version ($value)!"
            }
        }
        ElseIf ($key -eq 'rua') {
            $ruas++
            Write-Informational "TLSRPT: Aggregate information will be sent to: $value"
        }
        Else {
            Write-BadNews "TLSRPT: An invalid token was specified ($token)."
        }
    }

    If ($versions -ne 1) {
        Write-BadNews "TLSRPT: The required `"v`" tag did not appear exactly once. (It appeared $versions times.)"
    }
    If ($ruas -eq 0) {
        Write-BadNews 'TLSRPT: The required "rua" tag was not found!'
    }
}

Function Test-SpfRecord
{
    [CmdletBinding()]
    [OutputType([Void])]
    [Alias('Test-SenderIdRecord')]
    Param(
        [Parameter(Mandatory, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias('Name')]
        [String] $DomainName,

        [Alias('Recurse', 'CountSpfDnsLookups')]
        [Switch] $CountDnsLookups,

        [Alias('CD', 'DnssecCD', 'NoDnssec', 'DisableDnssec')]
        [Switch] $DisableDnssecVerification,

        [Parameter(DontShow)]
        [ref] $Recursions,

        [Parameter(DontShow)]
        [ref] $DnsLookups
    )

    # This is a recursive function. We do not expect the user to specify the
    # $Recursions or $DnsLookups parameters themselves (in fact, they're hidden
    # from help because they're for internal use only). Thus, if an explicit
    # value is not specified, re-call this function with one.
    If ($CountDnsLookups)
    {
        If ($null -eq $Recursions)
        {
            $r = -1        # it will be incremented to zero on the first run.
            $d = 0
            Test-SpfRecord -DomainName $DomainName -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions ([ref]$r) -DnsLookups ([ref]$d)
            Return    # do not recurse
        }
        Else {
            # PowerShell requires us to use the Value property to get the content
            # of a variable passed by reference.
            $Recursions.Value++
        }
    }

    #region Fetch the SPF record.
    # For historical reasons, we can also fetch Sender ID records. That was
    # Microsoft's failed attempt to make an "SPF 2.0". It can operate on either
    # of the two MailFrom headers, or both. It never really took off. Support
    # for Sender ID may be removed from this module in the future.
    $DnsLookup = Invoke-GooglePublicDnsApi "$DomainName" 'TXT' -Debug:$DebugPreference -DisableDnssecVerification:$DisableDnssecVerification
    $NoSPF = $false

    $SpfRecord = ($DnsLookup.Answer | Where-Object type -eq 16).Data | Where-Object {$_ -CLike "v=spf1 *" -or $_ -CLike "spf2.0/*"}
    If ($SpfRecord -CLike "v=spf1 *") {
        $RecordType = 'SPF'
    }
    ElseIf ($SpfRecord -CLike "spf2.0/*") {
        $RecordType = 'Sender ID'
    }
    Else {
        $RecordType = 'SPF'
        $NoSPF = $true
    }

    # Add indentation when doing recursive SPF lookups.
    If ($CountDnsLookups) {
        $RecordType = "$('├──' * $Recursions.Value)$RecordType"
    }
    #endregion

    #region DNSSEC check
    If (-Not $DisableDnssecVerification) {
        If ($DnsLookup.AD) {
            Write-GoodNews "${RecordType}: This DNS lookup is secure."
        }
        Else {
            Write-BadPractice "${RecordType}: This DNS lookup is insecure. Enable DNSSEC for this domain."
        }
    }
    #endregion

    If ($DnsLookup.PSObject.Properties.Name -NotContains 'Answer' -or $DnsLookup.Status -eq 3)
    {
        Write-BadNews "${RecordType}: No TXT records were found for $DomainName!"
        Return
    }
    If ($NoSPF)
    {
        Write-BadNews "${RecordType}: A TXT record was found for $DomainName, but it is not an SPF record!"
    }

    Write-Verbose "Checking the $RecordType record: `"$SpfRecord`""
    ForEach ($token in ($SpfRecord -Split ' ')) {
        #region Check SPF versions
        If ($token -Eq "v=spf1") {
            Write-GoodNews "${RecordType}: This is an SPF version 1 record."
        }
        ElseIf ($token -Eq "spf2.0/pra") {
            Write-BadPractice "${RecordType}: Sender ID records are historic and should be replaced with SPF TXT records."
            Write-GoodNews "${RecordType}: This is a Sender ID record checking the purported return address."
        }
        ElseIf ($token -Eq "spf2.0/mfrom") {
            Write-BadPractice "${RecordType}: Sender ID records are historic and should be replaced with SPF TXT records."
            Write-GoodNews "${RecordType}: This is a Sender ID record checking the mail From: address."
        }
        ElseIf ($token -Eq "spf2.0/pra,mfrom" -Or $token -Eq "spf2.0/mfrom,pra") {
            Write-BadPractice "${RecordType}: Sender ID records are historic and should be replaced with SPF TXT records."
            Write-GoodNews "${RecordType}: This is a Sender ID record checking the mail From: and purported return addresses (like SPF)."
        }
        #endregion

        #region Check redirect modifier.
        # If we're using the -CountDnsLookups/-Recurse parameter, this function
        # will be recursive and check the redirected SPF record.
        ElseIf ($token -Like 'redirect=*') {
            $Domain = ($token -Split '=')[1]
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }

            Write-Informational "${RecordType}: Use the SPF record at $Domain instead.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            If ($CountDnsLookups) {
                Test-SpfRecord $Domain -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions $Recursions -DnsLookups $DnsLookups
            }
        }
        #endregion

        #region Check A tokens.
        ElseIf ($token -Match '^[\+\-\?\~]?a([:/]*)' -and $token -NotMatch "all$")
        {
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }

            If ($token -Match "^\+?a$") {
                Write-GoodNews "${RecordType}: Accept mail from $DomainName's IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "-a") {
                Write-GoodNews "${RecordType}: Reject mail from $DomainName's IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "~a") {
                Write-BadPractice "${RecordType}: Accept but mark mail from $DomainName's IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "?a") {
                Write-BadPractice "${RecordType}: No opinion on mail from $DomainName's IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Match "^\+?a:*") {
                Write-GoodNews "${RecordType}: Accept mail from $($token -Replace '\+' -Replace 'a:')'s IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "-a:*") {
                Write-GoodNews "${RecordType}: Reject mail from $($token -Replace '-a:')'s IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "~a:*") {
                Write-BadPractice "${RecordType}: Accept but mark mail from $($token -Replace '?a:')'s IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "?a:*") {
                Write-BadPractice "${RecordType}: No opinion on mail from $($token -Replace '?a:')'s IP address(es).$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Match "/") {
                $mask = ($token -Split '/')[1]
                If ($token -Match "^\+?a/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Accept mail from all hosts in the same IPv4 /$mask as $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^-a/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Reject mail from all hosts in the same IPv4 /$mask as $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^~a/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from all hosts in the same IPv4 /$mask as $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^?a/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: No opinion on mail from all hosts in the same IPv4 /$mask as $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^\+?a:*/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Accept mail from all hosts in the same IPv4 /$mask as $($token -Replace '\+' -Replace 'a:' -Replace '/[0-3]?[0-9]').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^-a:*/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Reject mail from all hosts in the same IPv4 /$mask as $($token -Replace '-a:' -Replace '/[0-3]?[0-9]').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^~a:*/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from all hosts in the same IPv4 /$mask as $($token -Replace '~a:' -Replace '/[0-3]?[0-9]').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^?a:*/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: No opinion on mail from all hosts in the same IPv4 /$mask as $($token -Replace '?a:' -Replace '/[0-3]?[0-9]').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                Else {
                    Write-BadNews "${RecordType}: PermError while processing the A token $token."
                    Return
                }
            }
            Else {
                Write-BadNews "${RecordType}: PermError while processing the A token $token."
                Return
            }
        }
        #endregion

        #region Check MX tokens.
        ElseIf ($token -Match '^[\+\-\?\~]?mx([:/]*)')
        {
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }

            If ($token -Match "^\+?mx$") {
                Write-GoodNews "${RecordType}: Accept mail from $DomainName's MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "-mx") {
                Write-GoodNews "${RecordType}: Reject mail from $DomainName's MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "?mx") {
                Write-BadPractice "${RecordType}: No opinion on mail from $DomainName's MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "~mx") {
                Write-BadPractice "${RecordType}: Accept but mark mail from $DomainName's MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Match "^\+?mx:.*$") {
                Write-GoodNews "${RecordType}: Accept mail from $($token -Replace '\+' -Replace 'mx:')'s MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "-mx:.*$") {
                Write-GoodNews "${RecordType}: Reject mail from $($token -Replace '-mx:')'s MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "~mx:.*$") {
                Write-BadPractice "${RecordType}: Accept but mark mail from $($token -Replace '?mx:')'s MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "?mx:.*$") {
                Write-BadPractice "${RecordType}: No opinion on mail from $($token -Replace '?mx:')'s MX servers.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Match "/") {
                $mask = ($token -Split '/')[1]
                If ($token -Match "^\+?mx/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Accept mail from all hosts in the same IPv4 /$mask as $DomainName's MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^-mx/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Reject mail from all hosts in the same IPv4 /$mask as $DomainName's MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^\?mx/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: No opinion on mail from all hosts in the same IPv4 /$mask as $DomainName's MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^~mx/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from all hosts in the same IPv4 /$mask as $DomainName's MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^\+?mx:.*/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Accept mail from all hosts in the same IPv4 /$mask as $($token -Replace '\+' -Replace 'mx:' -Replace '/[0-3]?[0-9]')'s MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^-mx:.*/[0-3]?[0-9]$") {
                    Write-GoodNews "${RecordType}: Reject mail from all hosts in the same IPv4 /$mask as $($token -Replace '-mx:' -Replace '/[0-3]?[0-9]')'s MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^\?mx:.*/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: No opinion on mail from all hosts in the same IPv4 /$mask as $($token -Replace '?mx:' -Replace '/[0-3]?[0-9]')'s MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                ElseIf ($token -Match "^~mx:.*/[0-3]?[0-9]$") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from all hosts in the same IPv4 /$mask as $($token -Replace '?mx:' -Replace '/[0-3]?[0-9]')'s MX records.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                }
                Else {
                    Write-BadNews "${RecordType}: PermError while processing the MX token $token."
                    Return
                }
            }
            Else {
                Write-BadNews "${RecordType}: PermError while processing the MX token $token."
                Return
            }
        }
        #endregion

        #region Check exists tokens
        ElseIf ($token -Match "^[\+\-\?\~]?exists:.*")
        {
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }

            If ($token -Match "^\+?exists:.*") {
                Write-GoodNews "${RecordType}: Accept mail if $($token -Replace '\+' -Replace 'exists:') resolves to an A record.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "-exists:*") {
                Write-GoodNews "${RecordType}: Reject mail if $($token -Replace '-exists:') resolves to an A record.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "~exists:*") {
                Write-BadPractice "${RecordType}: Accept but mark mail if $($token -Replace '~exists:') resolves to an A record.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "?exists:*") {
                Write-BadPractice "${RecordType}: No opinion if $($token -Replace '?exists:') resolves to an A record.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            Else {
                Write-BadNews "${RecordType}: PermError while processing the Exists token $token."
                Return
            }
        }
        #endregion

        #region Check ip4: and ip6: tokens
        ElseIf ($token -Match "^[\+\-\?\~]?ip4:*") {
            If ($token -Match "/" -And -Not ($token -Like "*/32")) {
                $ip4net = $token -Replace 'ip4:'
                If ($token -Match "^\+?ip4:.*") {
                    Write-GoodNews "${RecordType}: Accept mail from the IPv4 subnet $ip4net."
                }
                ElseIf ($token -Like "-ip4:*") {
                    Write-GoodNews "${RecordType}: Reject mail from the IPv4 subnet $ip4net."
                }
                ElseIf ($token -Like "~ip4:*") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from the IPv4 subnet $ip4net."
                }
                ElseIf ($token -Like "?ip4:*") {
                    Write-BadPractice "${RecordType}: No opinion on mail from the IPv4 subnet $ip4net."
                }
                Else {
                    Write-BadNews "${RecordType}: PermError while processing the IPv4 token $token."
                    Return
                }
            } Else {
                $ip4addr = $token -Replace '[\+\-\~\?]?ip4:' -Replace '/32'
                If ($token -Match "^\+?ip4:.*") {
                    Write-GoodNews "${RecordType}: Accept mail from the IPv4 address $ip4addr."
                }
                ElseIf ($token -Like "-ip4:*") {
                    Write-GoodNews "${RecordType}: Reject mail from the IPv4 address $ip4addr."
                }
                ElseIf ($token -Like "~ip4:*") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from the IPv4 address $ip4addr."
                }
                ElseIf ($token -Like "?ip4:*") {
                    Write-BadPractice "${RecordType}: No opinion on mail from the IPv4 address $ip4addr."
                }
                Else {
                    Write-BadNews "${RecordType}: PermError: Could not parse the IPv4 token $token."
                    Return
                }
            }
        }
        ElseIf ($token -Match "^[\+\-\?\~]?ip6:*") {
            If ($token -Match "/" -And -Not ($token -Like "*/128")) {
                $ip6net = $token -Replace 'ip6:'
                If ($token -Match "^\+?ip6:*") {
                    Write-GoodNews "${RecordType}: Accept mail from the IPv6 subnet $ip6net."
                }
                ElseIf ($token -Like "-ip6:*") {
                    Write-GoodNews "${RecordType}: Reject mail from the IPv6 subnet $ip6net."
                }
                ElseIf ($token -Like "~ip6:*") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from the IPv6 subnet $ip6net."
                }
                ElseIf ($token -Like "?ip6:*") {
                    Write-BadPractice "${RecordType}: No opinion on mail from the IPv6 subnet $ip6net."
                }
                Else {
                    Write-BadNews "${RecordType}: PermError while processing the IPv6 token $token."
                    Return
                }
            } Else {
                $ip6addr = $token -Replace 'ip6:' -Replace '/128'
                If ($token -Match "^\+?ip6:*") {
                    Write-GoodNews "${RecordType}: Accept mail from the IPv6 address $ip6addr."
                }
                ElseIf ($token -Like "-ip6:*") {
                    Write-GoodNews "${RecordType}: Reject mail from the IPv6 address $ip6addr."
                }
                ElseIf ($token -Like "~ip6:*") {
                    Write-BadPractice "${RecordType}: Accept but mark mail from the IPv6 address $ip6addr."
                }
                ElseIf ($token -Like "?ip6:*") {
                    Write-BadPractice "${RecordType}: No opinion on mail from the IPv6 address $ip6addr."
                }
                Else {
                    Write-BadNews "${RecordType}: PermError while processing the IPv6 token $token."
                    Return
                }
            }
        }
        #endregion

        #region Check PTR tokens
        # The PTR mechanism is deprecated and should be avoided whenever possible.
        ElseIf ($token -Match "^[\+\-\?\~]?ptr(:.*)?") {
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }

            If ($token -Match "^\+?ptr$") {
                Write-BadPractice "${RecordType}: Accept mail from IP's that have a reverse DNS record ending in $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "-ptr") {
                Write-BadPractice "${RecordType}: Reject mail from IP's that have a reverse DNS record ending in $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "~ptr") {
                Write-BadPractice "${RecordType}: Accept but mark mail from IP's that have a reverse DNS record ending in $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Eq "?ptr") {
                Write-BadPractice "${RecordType}: No opinion on mail from IP's that have a reverse DNS record ending in $DomainName.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Match "^\+?ptr:.*") {
                Write-BadPractice "${RecordType}: Accept mail from IP's that have a reverse DNS record ending in $($token -Replace '\+' -Replace 'ptr:').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "-ptr:*") {
                Write-BadPractice "${RecordType}: Reject mail from IP's that have a reverse DNS record ending in $($token -Replace '-ptr:').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "~ptr:*") {
                Write-BadPractice "${RecordType}: Accept but mark mail from IP's that have a reverse DNS record ending in $($token -Replace '-ptr:').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            ElseIf ($token -Like "?ptr:*") {
                Write-BadPractice "${RecordType}: No opinion on mail from IP's that have a reverse DNS record ending in $($token -Replace '\?ptr:').$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
            }
            Else {
                Write-BadNews "${RecordType}: PermError while processing the PTR token $token."
                Return
            }
        }
        #endregion

        #region Check include: tokens
        # When running with the -CountDnsLookups/-Recurse parameter, the values
        # of the "include:" tokens will be checked recursively.
        ElseIf ($token -Match "^[\+\-\?\~]?include\:") {
            If ($CountDnsLookups) {
                $DnsLookups.Value++
            }
            $NextRecord = $token -Replace '^[\+\-\~\?]?include:',''

            If ($token -Match "^\+?include:*") {
                Write-GoodNews "${RecordType}: Accept mail that passes the SPF record at $nextRecord.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                If ($CountDnsLookups) {
                    Test-SpfRecord -DomainName $NextRecord -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions $Recursions -DnsLookups $DnsLookups
                }
            }
            ElseIf ($token -Like "-include:*") {
                Write-GoodNews "${RecordType}: Reject mail that passes the SPF record at $NextRecord.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                If ($CountDnsLookups) {
                    Test-SpfRecord -DomainName $NextRecord -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions $Recursions -DnsLookups $DnsLookups
                }
            }
            ElseIf ($token -Like "~include:*") {
                Write-BadPractice "${RecordType}: Accept but mark mail that passes the SPF record at $NextRecord.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                If ($CountDnsLookups) {
                    Test-SpfRecord -DomainName $NextRecord -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions $Recursions -DnsLookups $DnsLookups
                }
            }
            ElseIf ($token -Like "?include:*") {
                Write-BadPractice "${RecordType}: No opinion on mail that passes the SPF record at $NextRecord.$(Write-DnsLookups $DnsLookups -Enabled:$CountDnsLookups)"
                If ($CountDnsLookups) {
                    Test-SpfRecord -DomainName $NextRecord -CountDnsLookups:$CountDnsLookups -DisableDnssecVerification:$DisableDnssecVerification -Recursions $Recursions -DnsLookups $DnsLookups
                }
            }
            Else {
                Write-BadNews "${RecordType}: PermError while processing the Include token $token"
                Return
            }
        }
        #endregion

        #region Check for the "all" token.
        ElseIf ($token -Match "^[\+\-\?\~]?all")
        {
            If ($token -Match "^\+?all") {
                Write-BadPractice "${RecordType}: Accept all other mail."
            } ElseIf ($token -Eq "-all") {
                Write-GoodNews "${RecordType}: Reject all other mail."
            } ElseIf ($token -Eq "~all") {
                Write-BadPractice "${RecordType}: Accept but mark all other mail (this domain is likely testing SPF)."
            } ElseIf ($token -Eq "?all") {
                Write-BadPractice "${RecordType}: Do whatever with all other mail."
            } Else {
                Write-BadNews "${RecordType}: PermError while processing the All token $token"
                Return
            }
        }
        #endregion

        #region Check the exp= modifier
        # We will always attempt to resolve this and return the custom error
        # message. Note that this one does not count toward the ten DNS lookup
        # limit of SPF.
        ElseIf ($token -Like "exp=*")
        {
            $ExplanationRecord  = $token -Replace 'exp='
            $ExplanationMessage = ((Invoke-GooglePublicDnsApi $ExplanationRecord 'TXT').Answer | Where-Object Type -eq 16).Data
            Write-Informational "${RecordType}: Include this explanation with SPF failures: `"$ExplanationMessage`""
        }
        #endregion

        ElseIf ($token.Length -gt 0) {
            Write-BadNews "${RecordType}: PermError while processing the unknown token $token"
            Return
        }
    }

    # If this is the first instance of Test-SpfRecord (that is, we are not in
    # the middle of some recursion), then print the number of DNS lookups and
    # remove the script-level counter variable.
    If ($CountDnsLookups)
    {
        If ($Recursions.Value -gt 0) {
            $Recursions.Value--
        }
        ElseIf ($DnsLookups.Value -gt 10) {
            Write-BadNews "${RecordType}: PermError due to too many DNS lookups. $($DnsLookups.Value) lookups were required, but only 10 are allowed."
        }
    }
    Return
}

# SIG # Begin signature block
# MIIo5gYJKoZIhvcNAQcCoIIo1zCCKNMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCmIptgDmi/Mm9v
# EIeNOXUBiVQYkvpENF5vXFrXw54AWaCCI60wggR+MIIC5qADAgECAhEApna5vdQ8
# txEq0UQhUxLsMzANBgkqhkiG9w0BAQwFADBBMQswCQYDVQQGEwJVUzEQMA4GA1UE
# ChMHQ2VydGVyYTEgMB4GA1UEAxMXQ2VydGVyYSBDb2RlIFNpZ25pbmcgQ0EwHhcN
# MjIxMTI1MDAwMDAwWhcNMjUxMTI0MjM1OTU5WjBPMQswCQYDVQQGEwJVUzEUMBIG
# A1UECAwLQ29ubmVjdGljdXQxFDASBgNVBAoMC0NvbGluIENvZ2xlMRQwEgYDVQQD
# DAtDb2xpbiBDb2dsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIS0nGDy1zQpFKyt
# Jcg1PiDfvpNR79NCbfgewfNj/SLANVb3XbggjeibCl1fcefKLnXFv0DXHIKjYg0e
# hcFMbUQ1hqpwnnWQji1DcLeshAMdvWmTguYmtL6P4ik/BQDUuaOCAY8wggGLMB8G
# A1UdIwQYMBaAFP7HyA+eaTU9w8t0+WyaszQGqVwJMB0GA1UdDgQWBBSO8z1ie4Xj
# RAjUjX9ctrNH9aglYzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNV
# HSUEDDAKBggrBgEFBQcDAzBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgJlMCUwIwYI
# KwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEEATBIBgNV
# HR8EQTA/MD2gO6A5hjdodHRwOi8vQ2VydGVyYS5jcmwuc2VjdGlnby5jb20vQ2Vy
# dGVyYUNvZGVTaWduaW5nQ0EuY3JsMIGABggrBgEFBQcBAQR0MHIwQwYIKwYBBQUH
# MAKGN2h0dHA6Ly9DZXJ0ZXJhLmNydC5zZWN0aWdvLmNvbS9DZXJ0ZXJhQ29kZVNp
# Z25pbmdDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9DZXJ0ZXJhLm9jc3Auc2Vj
# dGlnby5jb20wDQYJKoZIhvcNAQEMBQADggGBAAslTgxzcZ0FYetE3IOghFsEtGV+
# yEM03ZrGFRGt7/DmHe4MK15XUsORJzN60eyNzxchQhV1S90jqQflkl6ImuvdaRve
# 586ZhYtW4tl2+2YbM26jwVqB9tT06W1SHb03+Vb29jjRbp5r+w3lEXxzGC660MFk
# 1L8kRQcqKjt0izVeVm6qKfNVQyak5xWpeX8n8NVaCqVWfijWlLDr8Ydeg9XeJy4H
# c9OweQ7+seRJzr/MgHQ0SFuXaRrbk0v5UmyoH83LZt/qo+XnrU+XeX870UVxucTl
# AitkDB6t/dvmetmXQGE5stJMyIK5jgtMqQ/q/GIrTFYMmcAsXxNQh8uv+jFa0HhF
# PZVhhdRbximJQUPyKb7IMuAzwdw1jrTcAF1FbkLlHXdu7dohbSfsN8ZA5Cr397wN
# n7UBs939mMBb4ZR+nBPFhibj5RISssbICi8z3LNb6CNuayOn3PtG/NRcf5T8iFyW
# /XbipYDJcxuQKwP8HWmlVIfQooRP6HR+Doee+DCCBY0wggR1oAMCAQICEA6bGI75
# 0C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV
# BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG
# A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAw
# MFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD
# ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGln
# aUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuE
# DcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNw
# wrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs0
# 6wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e
# 5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtV
# gkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85
# tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+S
# kjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1Yxw
# LEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzl
# DlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFr
# b7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATow
# ggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiu
# HA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQE
# AwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
# Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2
# hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/
# Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNK
# ei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHr
# lnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4
# oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5A
# Y8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNN
# n3O3AamfV6peKOK5lDCCBd4wggPGoAMCAQICEAH9bTD8o8pRqBu8ZA41Ay0wDQYJ
# KoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
# MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
# ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
# aG9yaXR5MB4XDTEwMDIwMTAwMDAwMFoXDTM4MDExODIzNTk1OVowgYgxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0
# eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VS
# VHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00ytUINh4qogTQkt
# ZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NCtnbyqTsrkfji
# b9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQfjtTkUcYRZ0YI
# UcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM8Ny8nkz+rwWW
# NR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hmAUTnAU5GU5sz
# YPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiVZ4vuPVb+DNBp
# DxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9N6frXTpsNVzb
# QdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sFqV4Wg8y4Z+Lo
# E53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9HE0XvMnsQybQ
# v0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ+gQek9QmRkpQ
# gbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyXHAc/DVL17e8v
# gg8CAwEAAaNCMEAwHQYDVR0OBBYEFFN5v1qqK0rPVIDh2JvAnfKyA2bLMA4GA1Ud
# DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQBc
# 1HwNz/cBfUGZZQxzxVKfy/jPmQZ/G9pDFZ+eAlVXlhTxUjwnh5Qo7R86ATeidvxT
# UMCEm8ZrTrqMIU+ijlVikfNpFdi8iOPEqgv976jpS1UqBiBtVXgpGe5fMFxLJBFV
# /ySabl4qK+4LTZ9/9wE4lBSVQwcJ+2Cp7hyrEoygml6nmGpZbYs/CPvI0UWvGBVk
# kBIPcyguxeIkTvxY7PD0Rf4is+svjtLZRWEFwZdvqHZyj4uMNq+/DQXOcY3mpm8f
# bKZxYsXY0INyDPFnEYkMnBNMcjTfvNVx36px3eG5bIw8El1l2r1XErZDa//l3k1m
# EVHPma7sF7bocZGM3kn+3TVxohUnlBzPYeMmu2+jZyUhXebdHQsuaBs7gq/sg2eF
# 1JhRdLG5mYCJ/394GVx5SmAukkCuTDcqLMnHYsgOXfc2W8rgJSUBtN0aB5x3AD/Q
# 3NXsPdT6uz/MhdZvf6kt37kC9/WXmrU12sNnsIdKqSieI47/XCdr4bBP8wfuAC7U
# WYfLUkGV6vRH1+5kQVV8jVkCld1incK57loodISlm7eQxwwH3/WJNnQy1ijBsLAL
# 4JxMwxzW/ONptUdGgS+igqvTY0RwxI3/LTO6rY97tXCIrj4Zz0Ao2PzIkLtdmSL1
# UuZYxR+IMUPuiB3Xxo48Q2odpxjefT0W8WL5ypCo/TCCBjwwggQkoAMCAQICECFm
# 8IpR6/yrzI9EMJGpSw4wDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UE
# ChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNB
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIyMDkwNzAwMDAwMFoXDTMyMDkw
# NjIzNTk1OVowQTELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0NlcnRlcmExIDAeBgNV
# BAMTF0NlcnRlcmEgQ29kZSBTaWduaW5nIENBMIIBojANBgkqhkiG9w0BAQEFAAOC
# AY8AMIIBigKCAYEAvp9xPhzayPelQMu7ycbIP8Kls73mzciRa7hO+f06rZl7Xw4F
# DKuA1Cu7nen1GFCPuqRvCqEizDiO4/WnM4nQcfVFkfpXfZf24qUztHzq5qsxlwpK
# W/Dkksj+I9A15W1dFbmToYswFElXzmKHSnZXoYMz+R4ZSwmnVB/XsvUPaAFi2dCr
# KN54pMcsBweUOKFunKWkji/MMnnPJGebOF1fLeDgyEHQvYuzlVfOWU3xjMiZYfqY
# gi8jo28qa0IYR17SdFZIgUWRlKhJnNKwyXfY8kElpfpeSbjM20jLch1+UhPXwTU/
# 5yHwXvUCSW4idXEihxbcleNXbeO8wfwfNHn2of4Y1w4mShxHFhDu/kPmzDIkpPct
# AmDyJfJfcL1E+aRFqGYhJwCOiMNQE9dfDkYL11Rtue3zmcpkqKbH6P6EI3UQSG1t
# H0OqY65xpSadXS/yGoXqOOEQpDf/U3trlyqroxhUhm0dN82CBqSXqMa23scYns1O
# 3u2kSPPHIEULOVq5AgMBAAGjggFmMIIBYjAfBgNVHSMEGDAWgBRTeb9aqitKz1SA
# 4dibwJ3ysgNmyzAdBgNVHQ4EFgQU/sfID55pNT3Dy3T5bJqzNAapXAkwDgYDVR0P
# AQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUH
# AwMwIgYDVR0gBBswGTANBgsrBgEEAbIxAQICZTAIBgZngQwBBAEwUAYDVR0fBEkw
# RzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUNl
# cnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHEGCCsGAQUFBwEBBGUwYzA6BggrBgEF
# BQcwAoYuaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUFBQUNB
# LmNydDAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkq
# hkiG9w0BAQwFAAOCAgEAe11w9/hMUEgtubZdffaBE4vbRYL0hunnc2Yaup6rzig/
# GjVOaTA7gdoChGhuxDE0AoYMF1znfLBSuNrU6B8tO/ikxFprLayPz9IUmbhEd/Ry
# VbMimZiC7z74OfjIVx86Y279nJ0VmX6lgHvwc8QcAVMN00Qse97OD9EeWMuY+hB7
# 1mKUp6pTipoqKJD4+hs2fOxjXew9OBYu6wjlgK6kbuBo+R2T7EuYyyfWubg9Cpwg
# dzRSpWmRO5DMG+u0FojEtP8MITbtJ1bLOWZ0JVvGKDWqNLVBvxHE8DwaAx3IrlZ8
# 1lxLO3zEL/mpUnC6cdQlVkq3G7qdWfIdkaNhNAv3hu0tH3t8bLoXYDB6Kyp5hdGZ
# 1XAO7H4b7MVW1amciuBXys6/VvfWmR/9Wh1rjWuYtP+y94oLg1gEisa7+Qid2qy/
# WSKC7cjpzwmg+6BGb2oEAO56pZToRc5a8vE9XcMPMO6hxI+MGbpqioQ/Nwa+94Ep
# D2aGUkmqX3gP6kUBbvS4Pys0jLgKxlyZDfwJb+4CWQOoZaiZoLAr/Y9+9j2YkeQD
# rt1A2zEDgOHRLlXYQDPuVNSu014pt8yAMY1OnHQSrTKwBZ2Y5H8AOw1yyIsMQISq
# OcPiepvzMAwSMJtTedvFq51+kuBHgltH2AdDlPfT13i3CAqn3LcFhehUZU4VIPsw
# 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/bt1nz8MIIGwjCCBKqgAwIBAgIQ
# BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw
# MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp
# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X
# 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU
# UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa
# 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt
# XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60
# pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17
# cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY
# QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9
# c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw
# 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c
# kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR
# B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD
# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG
# BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq
# II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw
# T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH
# NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD
# MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB
# BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0
# ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF
# 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC
# QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc
# jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8
# wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF
# KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP
# 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP
# NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr
# moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2
# obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ
# uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMYIEjzCCBIsC
# AQEwVjBBMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHQ2VydGVyYTEgMB4GA1UEAxMX
# Q2VydGVyYSBDb2RlIFNpZ25pbmcgQ0ECEQCmdrm91Dy3ESrRRCFTEuwzMA0GCWCG
# SAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcN
# AQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUw
# LwYJKoZIhvcNAQkEMSIEIJLpJ4JsxF5yJqXKRhrTKKV8E2k/lrV1/4X/SIt1p2KR
# MAsGByqGSM49AgEFAARnMGUCMG6YydWOwEOnFcyHFqwSXhyoeQ5O7cB/45vfCxNl
# Zd3wPXZ0HYX2TT6PptjvgVXY3gIxAOplyqglH8maLI31NADdisZzCqXMHXipI822
# wbbCSh4GMUbEewisGYZ4OvnnG5D6e6GCAyAwggMcBgkqhkiG9w0BCQYxggMNMIID
# CQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7
# MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1l
# U3RhbXBpbmcgQ0ECEAVEr/OUnQg5pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAY
# BgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA1MjMw
# MDA2MzFaMC8GCSqGSIb3DQEJBDEiBCCehlLiVhTzvWnSFZYE4kxRideFkNb6f9t9
# 3u6dqBCV2jANBgkqhkiG9w0BAQEFAASCAgCgvyIGXQbZdIQyJmISy3Xza5OuyYix
# QHIGOosuWWbmyQzmcK9EUb9hYYUruhphujRb2zNe42r0MnJL7s38cMxOApAF04Vu
# k5xXS4WkrkSziZOq3PDD+gND+MsEqibTaSt9AAuKus0GXS2pej6aw/qzBLa8htRR
# Ty3hGudZ0Hro6Sjgn0yXXZ1CCtKOqqohXKrG8mDaPA+b6uOlVcxq3YY+qBcj2Xnh
# IU9E60+qV8CtknomtThHfkg9tLeJ0gmJh9Hj94DoiB0uEPaDRBjx4X61S/tO+zkT
# Q1A5yDJidrnKMT9HOiQrpC6yk5jZmc+20H3g6Db8vaPbEPPl/7IzzUb2dtGixZWm
# XiDbhYrC2qSXyaUIPDskAJ/picpGCuYZK9c9Ai6P1gvy5UjKv7+KIpKqEhIfcb+E
# 5aPK2/LN/tq9a964nkvB7MYCB78r48kBRHcNkpYO/mwfFp8OMEzF4bfgGBLfSDfU
# oTNCO2Q9IQ6gD+QaR93LcozNWk87fULj4sLU0aGDFcOVA+niVB0trz9j47zf7S7D
# clQT7PhVtr5e7m0JnLD+AaAm2NKQ9nuU+jTzAn6rlDEmZlmPjZWzBGsTS8B190P+
# 5ZqukyIlDNX3B0DLdjkKkYGP2r8DW4Cmp4PcX55V+mgeT7Pim+qnR1giUzr0tNQP
# nzDk/09tjPEWWw==
# SIG # End signature block