Private/Get-MailDomain.ps1

# Domain helpers: organisational domain (heuristic without a full public suffix list),
# normalisation, exact and subdomain matches, DMARC alignment, address parsing.

$script:MultiLabelSuffixes = @(
    'co.uk', 'org.uk', 'ac.uk', 'gov.uk', 'me.uk', 'net.uk', 'ltd.uk', 'plc.uk',
    'com.au', 'net.au', 'org.au', 'co.nz', 'com.br', 'com.mx', 'com.ar',
    'com.tr', 'com.cn', 'com.tw', 'com.hk', 'com.sg', 'co.jp', 'or.jp', 'ne.jp',
    'co.in', 'co.za', 'com.pl', 'co.il', 'com.ua'
)

function Get-OrganizationalDomain {
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][string]$Domain)

    $parts = ($Domain.ToLowerInvariant() -replace '\.$', '') -split '\.'
    if ($parts.Count -le 2) { return ($parts -join '.') }
    $last2 = $parts[-2..-1] -join '.'
    if ($script:MultiLabelSuffixes -contains $last2) { return ($parts[-3..-1] -join '.') }
    return $last2
}

function ConvertTo-NormalizedDomain {
    <#
    .SYNOPSIS
        Lower-case, trimmed, without trailing dot or angle brackets. $null if nothing comparable remains.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Domain)

    if ([string]::IsNullOrEmpty($Domain)) { return $null }
    $s = $Domain.Trim().ToLowerInvariant()
    $s = [regex]::Replace($s, '^[<"'']+|[>"'',;]+$', '')
    $s = [regex]::Replace($s, '\.+$', '')
    if (-not $s) { return $null }
    return $s
}

function Test-SameDomain {
    <#
    .SYNOPSIS
        Exact domain comparison. No substring, no pattern.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()][string]$A, [AllowNull()][string]$B)

    $x = ConvertTo-NormalizedDomain -Domain $A
    $y = ConvertTo-NormalizedDomain -Domain $B
    return ($null -ne $x -and $x -eq $y)
}

function Test-HostMatch {
    <#
    .SYNOPSIS
        Same domain or real subdomain, always at a dot boundary.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()][string]$A, [AllowNull()][string]$B)

    $x = ConvertTo-NormalizedDomain -Domain $A
    $y = ConvertTo-NormalizedDomain -Domain $B
    if ($null -eq $x -or $null -eq $y) { return $false }
    if ($x -eq $y) { return $true }
    return ($x.EndsWith('.' + $y) -or $y.EndsWith('.' + $x))
}

function Get-DmarcAlignment {
    <#
    .SYNOPSIS
        Strict, Relaxed or None; $null when one side is missing.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][string]$A, [AllowNull()][string]$B)

    if (-not $A -or -not $B) { return $null }
    $x = $A.ToLowerInvariant() -replace '\.$', ''
    $y = $B.ToLowerInvariant() -replace '\.$', ''
    if ($x -eq $y) { return 'Strict' }
    if ((Get-OrganizationalDomain -Domain $x) -eq (Get-OrganizationalDomain -Domain $y)) { return 'Relaxed' }
    return 'None'
}

function ConvertFrom-MailAddress {
    <#
    .SYNOPSIS
        Display name, address and domain from a From/Reply-To/Return-Path value.
    #>

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

    $decoded = ([regex]::Replace((ConvertFrom-EncodedWord -Text $Value), '\s+', ' ')).Trim()
    $m = [regex]::Match($decoded, '<([^<>\s@]+@[^<>\s@]+)>')
    $address = $null
    if ($m.Success) { $address = $m.Groups[1].Value }
    else {
        $bare = [regex]::Match($decoded, '[\w.+=-]+@[\w.-]+')
        if ($bare.Success) { $address = $bare.Value }
    }
    $name = $null
    if ($m.Success -and $m.Index -gt 0) {
        $name = $decoded.Substring(0, $m.Index).Trim()
        $name = [regex]::Replace($name, '^"(.*)"$', '$1').Trim()
        if (-not $name) { $name = $null }
    }
    $domain = $null
    if ($address) {
        $domain = ($address -split '@')[-1].ToLowerInvariant().TrimEnd('>', ';', ',')
        if (-not $domain) { $domain = $null }
    }
    $display = $decoded
    if ($name -and $address) { $display = '{0} <{1}>' -f $name, $address }
    elseif ($address) { $display = $address }

    [pscustomobject]@{
        PSTypeName = 'MailHeaderAnalyzer.Address'
        Name       = $name
        Address    = $address
        Domain     = $domain
        Display    = $display
    }
}