Private/Split-MailHeader.ps1
|
function Split-MailHeader { <# .SYNOPSIS Splits raw header text into fields (unfolding, tolerant of hard-wrapped copies). #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Text ) # Strip a byte order mark: exports from Windows tools and copies from Notepad # carry one regularly, and without this cut the first line fails the # field-name test and silently disappears. $normalized = [regex]::Replace($Text, '^\uFEFF', '') $normalized = [regex]::Replace($normalized, '\r\n?', "`n") $lines = $normalized -split "`n" $rawFields = New-Object System.Collections.Generic.List[string] $current = $null $hadBody = $false for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i] if ($line.Trim() -eq '') { # The first blank line after fields separates header and body (.eml pasted). if ($rawFields.Count -gt 0 -or $null -ne $current) { for ($j = $i + 1; $j -lt $lines.Count; $j++) { if ($lines[$j].Trim() -ne '') { $hadBody = $true; break } } break } continue } if ($line -match '^[ \t]') { # Classic folding whitespace. if ($null -ne $current) { $current += "`n" + $line } continue } if ($line -match '^[\x21-\x39\x3B-\x7E]+:') { if ($null -ne $current) { $rawFields.Add($current) } $current = $line continue } # mbox separator "From ..." before the first field is skipped. if ($null -eq $current) { continue } # A line without field name and without leading whitespace: hard-wrapped # copy (for example from a mail client dialog); it belongs to the predecessor. $current += ' ' + $line.Trim() } if ($null -ne $current) { $rawFields.Add($current) } $fields = foreach ($raw in $rawFields) { $idx = $raw.IndexOf(':') [pscustomobject]@{ PSTypeName = 'MailHeaderAnalyzer.HeaderField' Name = $raw.Substring(0, $idx).Trim() Value = ([regex]::Replace($raw.Substring($idx + 1), '\n[ \t]+', ' ')).Trim() Raw = $raw } } [pscustomobject]@{ Fields = @($fields) HadBody = $hadBody } } function Get-HeaderField { <# .SYNOPSIS Returns the header fields with the given name (case-insensitive); -First returns only the first. #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Fields, [Parameter(Mandatory)] [string]$Name, [switch]$First ) $found = @($Fields | Where-Object { $_.Name -ieq $Name }) if ($First) { if ($found.Count -gt 0) { return $found[0] } return $null } return $found } function Get-HeaderValue { <# .SYNOPSIS Value of the first field with the given name, or $null. #> [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Fields, [Parameter(Mandatory)] [string]$Name ) $field = Get-HeaderField -Fields $Fields -Name $Name -First if ($null -eq $field) { return $null } return $field.Value } |