Public/Email/Test-SPFRecord.ps1

<#
Copyright © 2024 Integris. For internal company use only. All rights reserved.
#>


FUNCTION Test-SPFRecord {

    <#
    .SYNOPSIS
        Checks if an IP address is authorized to send email for a domain based on its SPF record.
     
    .PARAMETER Domain
        The domain to check the SPF record for.
     
    .PARAMETER IPAddress
        The IP address to verify against the SPF record.
     
    .EXAMPLE
        Test-SPFRecord -Domain "example.com" -IPAddress "230.128.11.31"
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string[]]$Domain,
        
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress[]]$IPAddress
    )

    Function Test-IndividualSPFRecord {
        
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$Domain,
        
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]$IPAddress
    )

    FUNCTION Test-IPAddressInCIDR {
        PARAM (
            [Parameter(Mandatory=$true)]
            [string]$IpAddress,

            [Parameter(Mandatory=$true)]
            [string]$CIDR
        )

        try {
            # Convert IP address to numerical representation
            $ip = [System.Net.IPAddress]::Parse($IpAddress)
            $ipBytes = $ip.GetAddressBytes()
            $ipLong = ([long]$ipBytes[0] -shl 24) + ([long]$ipBytes[1] -shl 16) + ([long]$ipBytes[2] -shl 8) + [long]$ipBytes[3]

            # Parse CIDR range and get network address and subnet mask
            $cidrParts = $CIDR.Split('/')
            $networkAddress = [System.Net.IPAddress]::Parse($cidrParts[0])
            $prefixLength = [int]$cidrParts[1]

            # Calculate subnet mask
            $subnetMaskLong = -1 -shl (32 - $prefixLength)

            # Convert network address to numerical representation
            $networkBytes = $networkAddress.GetAddressBytes()
            $networkLong = ([long]$networkBytes[0] -shl 24) + ([long]$networkBytes[1] -shl 16) + ([long]$networkBytes[2] -shl 8) + [long]$networkBytes[3]

            # Perform bitwise operations and compare
            if (($ipLong -band $subnetMaskLong) -eq ($networkLong -band $subnetMaskLong)) {
                return $true
            } else {
                return $false
            }
        } catch {
            Write-Error "Invalid IP address or CIDR range format: $($_.Exception.Message)"
            return $false
        }
    }
        
    try {
        # Resolve TXT records for the domain
        $txtRecords = Resolve-DnsName -Name $Domain -Type TXT -ErrorAction Stop | 
            Where-Object { $_.Strings -like 'v=spf1*' }
        
        if (-not $txtRecords) {
            #Write-Warning "No SPF record found for $Domain"
            return $false
        }
        
        # Get the SPF record
        $spfRecord = $txtRecords.Strings | Where-Object { $_ -like 'v=spf1*' }
        
        if ($spfRecord.Count -eq 0) {
            #Write-Warning "No valid SPF record found for $Domain"
            return $false
        }
        
        # Parse SPF record components
        $spfParts = $spfRecord -split '\s+'
        $ipValid = $false
        
        foreach ($part in $spfParts) {
            # Check for IP4 mechanism
            if ($part -like 'ip4:*') {
                $spfIP = $part -replace 'ip4:'
                
                # Handle CIDR notation
                if ($spfIP -like '*/[0-9]*') {
                    $ipNetwork = $spfIP.Split('/')[0]
                    $cidr = $spfIP.Split('/')[1]
                    
                    try {
                        IF (Test-IpAddressInCidr -IpAddress $IPAddress -Cidr $spfIP) { RETURN $True }
                    }
                    catch {
                        #Write-Warning "Error processing IP range $spfIP"
                    }
                }
                else {
                    # Direct IP comparison
                    if ($spfIP -eq $IPAddress) {
                        $ipValid = $true
                        break
                    }
                }
            }
            # Check for include mechanism
            elseif ($part -like 'include:*') {
                $includeDomain = $part -replace 'include:'
                try {
                    # Recursively check included domain
                    if (Test-IndividualSPFRecord -Domain $includeDomain -IPAddress $IPAddress) {
                        $ipValid = $true
                        break
                    }
                }
                catch {
                    #Write-Warning "Error checking include domain $includeDomain"
                }
            }
        }
        
        # Check for -all (hard fail)
        if ($spfRecord -like '*-all*') {
            return $ipValid
        }
        # Check for ~all (soft fail)
        elseif ($spfRecord -like '*~all*') {
            return $ipValid
        }
        # Check for ?all (neutral)
        elseif ($spfRecord -like '*?all*') {
            return $true
        }
        
        return $ipValid
    }
    catch {
        #Write-Error "Error checking SPF record: $_"
        return $false
    }
}

    $TotalCount = $Domain.Count * $IPAddress.Count
    $CurrentCount = 0

    $Results = @()
    FOREACH ($Entry in $Domain) {
        FOREACH ($Address in $IPAddress) { 
            
            Write-IntegrisProgressBar -TotalCount $TotalCount -CurrentCount $CurrentCount -Activity "Performing SPF Checks" -Status "Checking [$Entry] SPF Record for [$Address]" -ID 20938402 
            $CurrentCount++ 
    
            $Result = Test-IndividualSPFRecord -Domain $Entry -IPAddress $Address
            $PassResult = $False
            IF ($Result -eq $True) { $PassResult = $True } 
            $Results += [PSCustomObject]@{
                Domain = $Entry
                IPAddress = $Address
                SPFResult = $PassResult
            }
        }   
    }

    Write-Progress -ID 20938402 -Completed -Activity "Performing SPF Checks"
    RETURN $Results | Select Domain, IPAddress, SPFResult
}