Private/ConvertFrom-SpamHeader.ps1

# Spam filter headers: Microsoft Defender / EOP, SpamAssassin, Rspamd.
# The values are set by third parties, so they are decoded but never trusted.

$script:SclMeaning = @{
    '-1' = 'trusted: filtering bypassed (allow list, connection rule or internal sender)'
    '0'  = 'not spam'
    '1'  = 'not spam'
    '5'  = 'suspected spam: usually delivered to the junk folder'
    '6'  = 'suspected spam: usually delivered to the junk folder'
    '9'  = 'very likely spam: quarantine depending on policy'
}
$script:CatMeaning = @{
    NONE = 'no classification'; SPM = 'spam'; HSPM = 'spam (high confidence)'; PHSH = 'phishing'
    HPHSH = 'phishing (high confidence)'; MALW = 'malware'; SPOOF = 'spoofing'; DIMP = 'domain impersonation'
    UIMP = 'user impersonation'; GIMP = 'impersonation per mailbox intelligence'; BULK = 'bulk mail'
    AMP = 'malware caught by the anti-malware engine'; SAP = 'safe attachment (detonation)'
    FTBP = 'blocked by the attachment filter'; OSPM = 'outbound spam'; INTOS = 'intra-organization classified as phishing'
}
$script:SfvMeaning = @{
    NSPM = 'evaluated as not spam'; SPM = 'evaluated as spam'; BLK = "sender is on the recipient's block list"
    SKA = 'filtering skipped: sender on allow list'; SKB = 'filtering skipped: sender on block list'
    SKN = 'filtering skipped: pre-marked as not spam (e.g. transport rule)'; SKS = 'filtering skipped: pre-marked as spam'
    SKI = 'filtering skipped: intra-organization'; SKQ = 'released from quarantine'
}
$script:IpvMeaning = @{ CAL = 'submitting IP on the connection allow list'; NLI = 'IP without reputation entry' }
$script:DirMeaning = @{ INB = 'inbound'; OUT = 'outbound'; INT = 'internal' }

function ConvertFrom-ForefrontReport {
    <#
    .SYNOPSIS
        "CIP:203.0.113.25;CTRY:CH;..." into an ordered dictionary.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Value)

    $out = [ordered]@{}
    foreach ($part in ($Value -split ';')) {
        $idx = $part.IndexOf(':')
        if ($idx -le 0) { continue }
        $key = $part.Substring(0, $idx).Trim()
        if ($key -notmatch '^[A-Z]+$') { continue }
        $out[$key] = $part.Substring($idx + 1).Trim()
    }
    return $out
}

function Get-SpamAssassinTest {
    [CmdletBinding()]
    [OutputType([string[]])]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Value)

    $idx = $Value.ToLowerInvariant().IndexOf('tests=')
    if ($idx -lt 0) { return @() }
    $collected = New-Object System.Collections.Generic.List[string]
    foreach ($token in ($Value.Substring($idx + 6) -split '\s+')) {
        # The next "key=" ends the list.
        if ($token -match '^[A-Za-z_][A-Za-z0-9_]*=') { break }
        $collected.Add($token)
    }
    return @(($collected -join ' ') -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}

function Get-RspamdSymbol {
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Value)

    $out = New-Object System.Collections.Generic.List[object]
    foreach ($m in [regex]::Matches($Value, '[A-Z][A-Z0-9_]{2,}(\(([-\d.]+)\))?')) {
        if ($m.Groups[2].Success) {
            $out.Add([pscustomobject]@{ Name = $m.Value.Substring(0, $m.Value.IndexOf('(')); Score = $m.Groups[2].Value })
        }
    }
    return ,$out.ToArray()
}

function Get-SpamAssessment {
    <#
    .SYNOPSIS
        Decodes the spam filter headers present; $null when none are found.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param([Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Fields)

    $forefrontValue = Get-HeaderValue -Fields $Fields -Name 'X-Forefront-Antispam-Report'
    $antispamValue = Get-HeaderValue -Fields $Fields -Name 'X-Microsoft-Antispam'
    $spamStatus = Get-HeaderValue -Fields $Fields -Name 'X-Spam-Status'
    $rspamdValue = Get-HeaderValue -Fields $Fields -Name 'X-Spamd-Result'
    if (-not $rspamdValue) { $rspamdValue = Get-HeaderValue -Fields $Fields -Name 'X-Spam-Report' }

    if (-not $forefrontValue -and -not $antispamValue -and -not $spamStatus -and -not $rspamdValue) { return $null }

    $forefront = [ordered]@{}
    if ($forefrontValue) { $forefront = ConvertFrom-ForefrontReport -Value $forefrontValue }

    $scl = $null
    if ($forefront.Contains('SCL')) { $scl = $forefront['SCL'] }
    elseif ($antispamValue -and $antispamValue -match 'SCL:(-?\d+)') { $scl = $Matches[1] }
    $bcl = $null
    if ($antispamValue -and $antispamValue -match 'BCL:(\d+)') { $bcl = $Matches[1] }
    elseif ($forefront.Contains('BCL')) { $bcl = $forefront['BCL'] }

    $lookup = {
        param($table, $key)
        if ($null -eq $key) { return $null }
        if ($table.ContainsKey([string]$key)) { return $table[[string]$key] }
        return $null
    }

    $category = $null
    if ($forefront.Contains('CAT')) { $category = $forefront['CAT'] }
    $sfv = $null
    if ($forefront.Contains('SFV')) { $sfv = $forefront['SFV'] }
    $ipv = $null
    if ($forefront.Contains('IPV')) { $ipv = $forefront['IPV'] }
    $dir = $null
    if ($forefront.Contains('DIR')) { $dir = $forefront['DIR'] }
    $cip = $null
    if ($forefront.Contains('CIP')) { $cip = $forefront['CIP'] }
    $ctry = $null
    if ($forefront.Contains('CTRY')) { $ctry = $forefront['CTRY'] }

    $saScore = $null
    $saTests = @()
    if ($spamStatus) {
        if ($spamStatus -match 'score=(-?[\d.]+)') { $saScore = $Matches[1] }
        $saTests = Get-SpamAssassinTest -Value $spamStatus
    }
    $rspamdSymbols = @()
    if ($rspamdValue) { $rspamdSymbols = Get-RspamdSymbol -Value $rspamdValue }

    [pscustomobject]@{
        PSTypeName            = 'MailHeaderAnalyzer.SpamAssessment'
        Scl                   = $scl
        SclMeaning            = (& $lookup $script:SclMeaning $scl)
        Bcl                   = $bcl
        Category              = $category
        CategoryMeaning       = (& $lookup $script:CatMeaning $category)
        SpamFilterVerdict     = $sfv
        SpamFilterMeaning     = (& $lookup $script:SfvMeaning $sfv)
        IPVerdict             = $ipv
        IPVerdictMeaning      = (& $lookup $script:IpvMeaning $ipv)
        Direction             = $dir
        DirectionMeaning      = (& $lookup $script:DirMeaning $dir)
        ConnectingIP          = $cip
        Country               = $ctry
        Forefront             = $forefront
        SpamAssassinScore     = $saScore
        SpamAssassinTests     = @($saTests)
        RspamdSymbols         = @($rspamdSymbols)
    }
}