Public/Get-MailHeaderAnalysis.ps1

function Get-MailHeaderAnalysis {
    <#
    .SYNOPSIS
        Analyzes an email header: delivery chain, SPF/DKIM/DMARC/ARC results, Exchange hybrid
        classification, spam filter verdicts and anomalies. Runs entirely offline.
 
    .DESCRIPTION
        Parses the raw header of an email message (or a whole .eml file; the body is ignored)
        and returns an analysis object. Nothing leaves the machine: the cmdlet performs no DNS
        lookups and no HTTP requests, which makes it safe for headers that contain customer data.
 
        What it reads:
        - Received chain in chronological order, with delays per hop, TLS version and cipher,
          protocol class (RFC 3848), private IPs and provider detection.
        - Authentication-Results (RFC 8601), including the Microsoft variant without authserv-id,
          Received-SPF, DKIM-Signature tags, the ARC chain and compauth reason codes.
        - Whether the verification results actually come from the receiving server: the
          authserv-id must match a station of the delivery chain, otherwise the results are an
          unverified claim (a sender can prepend such a line).
        - DMARC alignment (strict/relaxed) of SPF and DKIM against the From domain.
        - Exchange Online hybrid headers (MessageDirectionality, AuthAs, AuthMechanism,
          CrossTenant-*), Microsoft Defender/EOP verdicts (SCL, BCL, CAT, SFV, IPV),
          SpamAssassin and Rspamd.
        - Anomalies: duplicate singleton fields, Unicode direction controls, weak DKIM hashes,
          expired signatures, clock skew, Reply-To mismatches and more, as Findings.
 
    .PARAMETER Header
        The raw header text. Accepts pipeline input line by line (for example from Get-Content),
        all lines are joined into one header.
 
    .PARAMETER Path
        Path to a file with the raw header or a complete .eml message. Accepts pipeline input
        from Get-ChildItem.
 
    .PARAMETER FromClipboard
        Reads the header from the clipboard (Windows). Copy the header in Outlook
        (File > Properties > Internet headers) or the webmail client, then run the cmdlet.
 
    .EXAMPLE
        Get-MailHeaderAnalysis -Path .\message.eml
 
        Analyzes the header of a saved message and shows the summary.
 
    .EXAMPLE
        Get-MailHeaderAnalysis -FromClipboard | Select-Object -ExpandProperty Hops | Format-Table
 
        Shows the delivery chain of the header currently in the clipboard.
 
    .EXAMPLE
        Get-Content .\header.txt | Get-MailHeaderAnalysis | Select-Object -ExpandProperty Findings
 
        Lists only the anomalies.
 
    .EXAMPLE
        Get-ChildItem .\samples\*.eml | Get-MailHeaderAnalysis | Select-Object Source, Spf, Dkim, Dmarc, AuthTrust
 
        Batch check of several messages.
 
    .EXAMPLE
        Get-MailHeaderAnalysis -Path .\message.eml | ConvertTo-MailHeaderReport | Set-Clipboard
 
        Builds a Markdown report for a ticket.
 
    .OUTPUTS
        MailHeaderAnalyzer.Analysis
 
    .LINK
        https://rafaelpfister.ch/en/tools/header-analyzer
    #>

    [CmdletBinding(DefaultParameterSetName = 'Text', HelpUri = 'https://rafaelpfister.ch/en/tools/header-analyzer')]
    [OutputType('MailHeaderAnalyzer.Analysis')]
    param(
        [Parameter(ParameterSetName = 'Text', Mandatory, ValueFromPipeline, Position = 0)]
        [AllowEmptyString()]
        [Alias('Text', 'Raw', 'InputObject')]
        [string[]]$Header,

        [Parameter(ParameterSetName = 'Path', Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'PSPath', 'LiteralPath')]
        [string[]]$Path,

        [Parameter(ParameterSetName = 'Clipboard', Mandatory)]
        [switch]$FromClipboard
    )

    begin {
        $collected = New-Object System.Collections.Generic.List[string]
    }

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'Text' {
                foreach ($chunk in $Header) { $collected.Add($chunk) }
            }
            'Path' {
                foreach ($p in $Path) {
                    $resolved = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($p)
                    if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) {
                        Write-Error -Message ('File not found: {0}' -f $p) -Category ObjectNotFound -TargetObject $p
                        continue
                    }
                    # ReadAllText honours a byte order mark and defaults to UTF-8.
                    $text = [System.IO.File]::ReadAllText($resolved)
                    Invoke-HeaderAnalysis -Text $text -Source $resolved
                }
            }
            'Clipboard' {
                if (-not $FromClipboard) { return }
                if (-not (Get-Command -Name Get-Clipboard -ErrorAction SilentlyContinue)) {
                    throw 'Get-Clipboard is not available on this platform. Use -Path or -Header instead.'
                }
                $text = Get-Clipboard -Raw
                if ([string]::IsNullOrWhiteSpace($text)) {
                    throw 'The clipboard is empty. Copy the message header first.'
                }
                Invoke-HeaderAnalysis -Text $text -Source 'Clipboard'
            }
        }
    }

    end {
        if ($PSCmdlet.ParameterSetName -eq 'Text') {
            $text = $collected -join "`n"
            if ([string]::IsNullOrWhiteSpace($text)) {
                throw 'No header text given.'
            }
            Invoke-HeaderAnalysis -Text $text -Source 'Text'
        }
    }
}