From 661f25150a1cc30d15343b6557697661c29dc993 Mon Sep 17 00:00:00 2001 From: "manuel.maier" Date: Tue, 13 Oct 2026 14:15:55 +0200 Subject: [PATCH] SMTPGraphRelay.ps1 aktualisiert --- SMTPGraphRelay.ps1 | 2406 +++++++++++++++----------------------------- 1 file changed, 821 insertions(+), 1585 deletions(-) diff --git a/SMTPGraphRelay.ps1 b/SMTPGraphRelay.ps1 index d9aa8d2..e783675 100644 --- a/SMTPGraphRelay.ps1 +++ b/SMTPGraphRelay.ps1 @@ -1,259 +1,219 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = "$PSScriptRoot\config.json" -) +[CmdletBinding()] +param([string]$ConfigPath = "$PSScriptRoot\config.json"); -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +#Requires -Version 5.1 + +$ErrorActionPreference = "Stop"; +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; function Get-LogSettings { - $maxSizeMB = 10 - $retentionDays = 30 - $cleanupHours = 12 + $maxSizeMB = 10; + $retentionDays = 30; + $cleanupHours = 12; try { if ($script:Config.Logging) { if ($script:Config.Logging.PSObject.Properties.Name -contains "MaxFileSizeMB") { - $v = [int]$script:Config.Logging.MaxFileSizeMB - if ($v -ge 1 -and $v -le 1024) { $maxSizeMB = $v } + $v = [int]$script:Config.Logging.MaxFileSizeMB; + if ($v -ge 1 -and $v -le 1024) { $maxSizeMB = $v; } } - if ($script:Config.Logging.PSObject.Properties.Name -contains "RetentionDays") { - $v = [int]$script:Config.Logging.RetentionDays - if ($v -ge 1 -and $v -le 3650) { $retentionDays = $v } + $v = [int]$script:Config.Logging.RetentionDays; + if ($v -ge 1 -and $v -le 3650) { $retentionDays = $v; } } - if ($script:Config.Logging.PSObject.Properties.Name -contains "CleanupHours") { - $v = [int]$script:Config.Logging.CleanupHours - if ($v -ge 1 -and $v -le 168) { $cleanupHours = $v } + $v = [int]$script:Config.Logging.CleanupHours; + if ($v -ge 1 -and $v -le 168) { $cleanupHours = $v; } } } } catch {} return [pscustomobject]@{ - MaxFileSizeMB = $maxSizeMB - RetentionDays = $retentionDays - CleanupHours = $cleanupHours - } + MaxFileSizeMB = $maxSizeMB; + RetentionDays = $retentionDays; + CleanupHours = $cleanupHours; + }; } function Invoke-LogMaintenance { - param( - [switch]$Force - ) + param([switch]$Force); - if (-not $script:Config -or -not $script:Config.Paths.Logs) { - return - } + if (-not $script:Config -or -not $script:Config.Paths.Logs) { return; } - $settings = Get-LogSettings - $logDir = $script:Config.Paths.Logs + $settings = Get-LogSettings; + $logDir = $script:Config.Paths.Logs; if (-not [IO.Path]::IsPathRooted($logDir)) { if ($PSScriptRoot) { - $logDir = Join-Path $PSScriptRoot $logDir + $logDir = Join-Path ($PSScriptRoot) $logDir; } else { - return + return; } } - New-Item -ItemType Directory -Path $logDir -Force | Out-Null + New-Item -ItemType Directory -Path ($logDir) -Force | Out-Null; if (-not $script:NextLogCleanup) { - $script:NextLogCleanup = [DateTime]::MinValue + $script:NextLogCleanup = [DateTime]::MinValue; } - if (-not $Force -and (Get-Date) -lt $script:NextLogCleanup) { - return - } + if (-not $Force -and (Get-Date) -lt $script:NextLogCleanup) { return; } - $cutoff = (Get-Date).AddDays(-1 * $settings.RetentionDays) + $cutoff = (Get-Date).AddDays(-1 * $settings.RetentionDays); + $files = Get-ChildItem -LiteralPath ($logDir) -File -Filter "SMTPGraphRelay-*.log" -ErrorAction SilentlyContinue; - foreach ($file in Get-ChildItem -LiteralPath $logDir -File -Filter "SMTPGraphRelay-*.log" -ErrorAction SilentlyContinue) { + foreach ($file in $files) { if ($file.LastWriteTime -lt $cutoff) { - try { - Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop - } catch {} + try { Remove-Item -LiteralPath ($file.FullName) -Force -ErrorAction Stop; } catch {} } } - $script:NextLogCleanup = (Get-Date).AddHours($settings.CleanupHours) + $script:NextLogCleanup = (Get-Date).AddHours($settings.CleanupHours); } function Rotate-LogIfNeeded { - param( - [Parameter(Mandatory)][string]$LogFile - ) + param([Parameter(Mandatory)][string]$LogFile); - if (-not (Test-Path -LiteralPath $LogFile)) { - return - } + if (-not (Test-Path -LiteralPath ($LogFile))) { return; } - $settings = Get-LogSettings + $settings = Get-LogSettings; try { - $file = Get-Item -LiteralPath $LogFile -ErrorAction Stop - $maxBytes = [int64]$settings.MaxFileSizeMB * 1024 * 1024 + $file = Get-Item -LiteralPath ($LogFile) -ErrorAction Stop; + $maxBytes = [int64]$settings.MaxFileSizeMB * 1024 * 1024; - if ($file.Length -lt $maxBytes) { - return + if ($file.Length -lt $maxBytes) { return; } + + $stamp = Get-Date -Format "yyyyMMdd-HHmmss"; + $dir = Split-Path ($LogFile) -Parent; + $archive = Join-Path ($dir) "SMTPGraphRelay-$stamp.log"; + + $counter = 1; + while (Test-Path -LiteralPath ($archive)) { + $archive = Join-Path ($dir) ("SMTPGraphRelay-{0}-{1}.log" -f $stamp, $counter); + $counter++; } - $stamp = Get-Date -Format "yyyyMMdd-HHmmss" - $dir = Split-Path $LogFile -Parent - $archive = Join-Path $dir "SMTPGraphRelay-$stamp.log" - - $counter = 1 - while (Test-Path -LiteralPath $archive) { - $archive = Join-Path $dir ("SMTPGraphRelay-{0}-{1}.log" -f $stamp, $counter) - $counter++ - } - - Move-Item -LiteralPath $LogFile -Destination $archive -Force - } - catch {} + Move-Item -LiteralPath ($LogFile) -Destination ($archive) -Force; + } catch {} } function Write-Log { param( [Parameter(Mandatory)][string]$Message, [ValidateSet("INFO","WARN","ERROR","DEBUG")][string]$Level = "INFO" - ) + ); - $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff" - $line = "[$ts] [$Level] $Message" - Write-Host $line + $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"; + $line = "[$ts] [$Level] $Message"; + Write-Host $line; try { if ($script:Config -and $script:Config.Paths.Logs) { - $logDir = $script:Config.Paths.Logs + $logDir = $script:Config.Paths.Logs; if (-not [IO.Path]::IsPathRooted($logDir)) { if ($PSScriptRoot) { - $logDir = Join-Path $PSScriptRoot $logDir + $logDir = Join-Path ($PSScriptRoot) $logDir; } else { - return + return; } } - New-Item -ItemType Directory -Path $logDir -Force | Out-Null - $logFile = Join-Path $logDir "SMTPGraphRelay.log" + New-Item -ItemType Directory -Path ($logDir) -Force | Out-Null; + $logFile = Join-Path ($logDir) "SMTPGraphRelay.log"; - # Mehrere SMTP-Runspaces und der Queue-Worker koennen gleichzeitig loggen. - # Ein benannter Mutex schuetzt Rotation und Schreibzugriff gemeinsam. - $mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log") - $lockTaken = $false + $mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log"); + $lockTaken = $false; try { - $lockTaken = $mutex.WaitOne(5000) - + $lockTaken = $mutex.WaitOne(5000); if ($lockTaken) { - Rotate-LogIfNeeded -LogFile $logFile - Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8 - Invoke-LogMaintenance + Rotate-LogIfNeeded -LogFile ($logFile); + Add-Content -LiteralPath ($logFile) -Value ($line) -Encoding UTF8; + Invoke-LogMaintenance; } - } - finally { - if ($lockTaken) { - try { $mutex.ReleaseMutex() } catch {} - } - - $mutex.Dispose() + } finally { + if ($lockTaken) { try { $mutex.ReleaseMutex(); } catch {} } + $mutex.Dispose(); } } - } - catch {} + } catch {} } function Resolve-PathFromConfig { - param([Parameter(Mandatory)][string]$Path) - if ([IO.Path]::IsPathRooted($Path)) { return $Path } - return (Join-Path $PSScriptRoot $Path) + param([Parameter(Mandatory)][string]$Path); + if ([IO.Path]::IsPathRooted($Path)) { return $Path; } + return (Join-Path ($PSScriptRoot) $Path); } function Test-IPv4InCidr { param( [Parameter(Mandatory)][System.Net.IPAddress]$Address, [Parameter(Mandatory)][string]$Cidr - ) + ); - if ($Address.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { - return $false - } + if ($Address.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { return $false; } + if ($Cidr -notmatch '^(.+)/(\d{1,2})$') { return $false; } - if ($Cidr -notmatch '^(.+)/(\d{1,2}); $') { return $false } + try { $network = [System.Net.IPAddress]::Parse($matches[1]); } catch { return $false; } + if ($network.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { return $false; } - try { - $network = [System.Net.IPAddress]::Parse($matches[1]) - } catch { - return $false - } + $prefix = [int]$matches[2]; + if ($prefix -lt 0 -or $prefix -gt 32) { return $false; } - if ($network.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { - return $false - } - - $prefix = [int]$matches[2] - if ($prefix -lt 0 -or $prefix -gt 32) { return $false } - - $ipBytes = $Address.GetAddressBytes() - $netBytes = $network.GetAddressBytes() + $ipBytes = $Address.GetAddressBytes(); + $netBytes = $network.GetAddressBytes(); for ($i = 0; $i -lt 4; $i++) { - $remaining = $prefix - ($i * 8) - if ($remaining -le 0) { break } + $remaining = $prefix - ($i * 8); + if ($remaining -le 0) { break; } - $bits = [Math]::Min(8, $remaining) - [byte]$mask = (0xFF -shl (8 - $bits)) -band 0xFF + $bits = [Math]::Min(8, $remaining); + [byte]$mask = (0xFF -shl (8 - $bits)) -band 0xFF; - if (($ipBytes[$i] -band $mask) -ne ($netBytes[$i] -band $mask)) { - return $false - } + if (($ipBytes[$i] -band $mask) -ne ($netBytes[$i] -band $mask)) { return $false; } } - - return $true + return $true; } function Test-ClientAllowed { - param([Parameter(Mandatory)][System.Net.IPAddress]$Address) + param([Parameter(Mandatory)][System.Net.IPAddress]$Address); - foreach ($entry in @($script:Config.Smtp.AllowedNetworks)) { - if ($entry -eq "*") { return $true } + $allowed = @($script:Config.Smtp.AllowedNetworks); + foreach ($entry in $allowed) { + if ($entry -eq "*") { return $true; } try { if ($entry -match '/') { - if (Test-IPv4InCidr -Address $Address -Cidr $entry) { return $true } + if (Test-IPv4InCidr -Address ($Address) -Cidr ($entry)) { return $true; } } elseif ([System.Net.IPAddress]::Parse($entry).Equals($Address)) { - return $true + return $true; } } catch {} } - return $false + return $false; } function Get-GraphConnection { if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) { - throw "Microsoft.Graph.Authentication ist nicht installiert." + throw "Microsoft.Graph.Authentication ist nicht installiert."; } - Import-Module Microsoft.Graph.Authentication -ErrorAction Stop + Import-Module Microsoft.Graph.Authentication -ErrorAction Stop; + $cert = Get-Item -LiteralPath ("Cert:\LocalMachine\My\{0}" -f $script:Config.Graph.CertificateThumbprint) -ErrorAction Stop; + + $ctx = Get-MgContext; + $needsConnect = $true; - $cert = Get-Item -LiteralPath ("Cert:\LocalMachine\My\{0}" -f $script:Config.Graph.CertificateThumbprint) -ErrorAction Stop - - $ctx = Get-MgContext - $needsConnect = $true if ($ctx) { if ($ctx.ClientId -eq $script:Config.Graph.ClientId -and $ctx.TenantId -eq $script:Config.Graph.TenantId -and $ctx.AuthType -eq "AppOnly") { - $needsConnect = $false + $needsConnect = $false; } } if ($needsConnect) { - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - Connect-MgGraph ` - -TenantId $script:Config.Graph.TenantId ` - -ClientId $script:Config.Graph.ClientId ` - -Certificate $cert ` - -NoWelcome | Out-Null + Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null; + Connect-MgGraph -TenantId ($script:Config.Graph.TenantId) -ClientId ($script:Config.Graph.ClientId) -Certificate ($cert) -NoWelcome | Out-Null; } } @@ -261,150 +221,118 @@ function Set-MimeSender { param( [Parameter(Mandatory)][byte[]]$MimeBytes, [Parameter(Mandatory)][string]$Sender - ) + ); + + $latin1 = [System.Text.Encoding]::GetEncoding(28591); + $text = $latin1.GetString($MimeBytes); + $separator = "`r`n`r`n"; + $idx = $text.IndexOf($separator); - # Headerbereich als Latin1 lesen, damit Bytes 1:1 erhalten bleiben. - $latin1 = [System.Text.Encoding]::GetEncoding(28591) - $text = $latin1.GetString($MimeBytes) - $separator = "`r`n`r`n" - $idx = $text.IndexOf($separator) if ($idx -lt 0) { - $separator = "`n`n" - $idx = $text.IndexOf($separator) + $separator = "`n`n"; + $idx = $text.IndexOf($separator); } - if ($idx -lt 0) { return $MimeBytes } + if ($idx -lt 0) { return $MimeBytes; } - $headers = $text.Substring(0, $idx) - $body = $text.Substring($idx + $separator.Length) + $headers = $text.Substring(0, $idx); + $body = $text.Substring($idx + $separator.Length); if ($headers -match '(?im)^From:.*(?:\r?\n[ \t].*)*') { - $headers = [regex]::Replace( - $headers, - '(?im)^From:.*(?:\r?\n[ \t].*)*', - "From: <$Sender>", - 1 - ) + $headers = [regex]::Replace($headers, '(?im)^From:.*(?:\r?\n[ \t].*)*', "From: <$Sender>", 1); } else { - $headers = "From: <$Sender>`r`n" + $headers + $headers = "From: <$Sender>`r`n" + $headers; } - return $latin1.GetBytes($headers + "`r`n`r`n" + $body) + return $latin1.GetBytes($headers + "`r`n`r`n" + $body); } function Get-QueueDirectories { - $queueRoot = Resolve-PathFromConfig $script:Config.Paths.Queue - $failedDir = Resolve-PathFromConfig $script:Config.Paths.Failed + $queueRoot = Resolve-PathFromConfig ($script:Config.Paths.Queue); + $failedDir = Resolve-PathFromConfig ($script:Config.Paths.Failed); return [pscustomobject]@{ - Root = $queueRoot - Incoming = Join-Path $queueRoot "incoming" - Pending = Join-Path $queueRoot "pending" - Processing = Join-Path $queueRoot "processing" - Failed = $failedDir - } + Root = $queueRoot; + Incoming = Join-Path ($queueRoot) "incoming"; + Pending = Join-Path ($queueRoot) "pending"; + Processing = Join-Path ($queueRoot) "processing"; + Failed = $failedDir; + }; } function Initialize-QueueDirectories { - $dirs = Get-QueueDirectories + $dirs = Get-QueueDirectories; + $folderList = @($dirs.Root, $dirs.Incoming, $dirs.Pending, $dirs.Processing, $dirs.Failed); - foreach ($dir in @( - $dirs.Root, - $dirs.Incoming, - $dirs.Pending, - $dirs.Processing, - $dirs.Failed - )) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null + foreach ($dir in $folderList) { + New-Item -ItemType Directory -Path ($dir) -Force | Out-Null; } - # Alte V1-Mails direkt aus queue\ nach pending migrieren. - foreach ($file in Get-ChildItem -LiteralPath $dirs.Root -Filter "*.eml" -File -ErrorAction SilentlyContinue) { - $target = Join-Path $dirs.Pending $file.Name - if (-not (Test-Path -LiteralPath $target)) { - Move-Item -LiteralPath $file.FullName -Destination $target -Force - } + $oldFiles = Get-ChildItem -LiteralPath ($dirs.Root) -Filter "*.eml" -File -ErrorAction SilentlyContinue; + foreach ($file in $oldFiles) { + $target = Join-Path ($dirs.Pending) $file.Name; + if (-not (Test-Path -LiteralPath ($target))) { Move-Item -LiteralPath ($file.FullName) -Destination ($target) -Force; } - $oldMeta = "$($file.FullName).json" - if (Test-Path -LiteralPath $oldMeta) { - $targetMeta = "$target.json" - if (-not (Test-Path -LiteralPath $targetMeta)) { - Move-Item -LiteralPath $oldMeta -Destination $targetMeta -Force - } + $oldMeta = "$($file.FullName).json"; + if (Test-Path -LiteralPath ($oldMeta)) { + $targetMeta = "$target.json"; + if (-not (Test-Path -LiteralPath ($targetMeta))) { Move-Item -LiteralPath ($oldMeta) -Destination ($targetMeta) -Force; } } - - Write-Log "Alte Queue-Mail nach pending migriert: $($file.Name)" + Write-Log "Alte Queue-Mail nach pending migriert: $($file.Name)"; } - # Nach Absturz/Neustart koennen Dateien in processing liegen. - # Sie werden wieder nach pending gestellt und spaeter erneut versucht. - foreach ($file in Get-ChildItem -LiteralPath $dirs.Processing -Filter "*.eml" -File -ErrorAction SilentlyContinue) { - $pendingPath = Join-Path $dirs.Pending $file.Name - $processingMeta = "$($file.FullName).json" - $pendingMeta = "$pendingPath.json" + $procFiles = Get-ChildItem -LiteralPath ($dirs.Processing) -Filter "*.eml" -File -ErrorAction SilentlyContinue; + foreach ($file in $procFiles) { + $pendingPath = Join-Path ($dirs.Pending) $file.Name; + $processingMeta = "$($file.FullName).json"; + $pendingMeta = "$pendingPath.json"; - if (Test-Path -LiteralPath $processingMeta) { - Move-Item -LiteralPath $processingMeta -Destination $pendingMeta -Force - } - - Move-Item -LiteralPath $file.FullName -Destination $pendingPath -Force - Write-Log "Processing-Mail nach Neustart zurueck nach pending gestellt: $($file.Name)" "WARN" + if (Test-Path -LiteralPath ($processingMeta)) { Move-Item -LiteralPath ($processingMeta) -Destination ($pendingMeta) -Force; } + Move-Item -LiteralPath ($file.FullName) -Destination ($pendingPath) -Force; + Write-Log "Processing-Mail nach Neustart zurueck nach pending gestellt: $($file.Name)" "WARN"; } } function Get-GraphFailureInfo { - param( - [Parameter(Mandatory)] - $ErrorRecord - ) + param([Parameter(Mandatory)]$ErrorRecord); - $statusCode = $null - $retryAfterSeconds = $null - $message = $ErrorRecord.Exception.Message + $statusCode = $null; + $retryAfterSeconds = $null; + $message = $ErrorRecord.Exception.Message; try { - $response = $ErrorRecord.Exception.Response - + $response = $ErrorRecord.Exception.Response; if ($response) { try { - if ($null -ne $response.StatusCode) { - $statusCode = [int]$response.StatusCode - } + if ($null -ne $response.StatusCode) { $statusCode = [int]$response.StatusCode; } } catch {} try { - $headers = $response.Headers - + $headers = $response.Headers; if ($headers) { - # HttpResponseMessage / HttpResponseHeaders try { - $values = $null + $values = $null; if ($headers.TryGetValues("Retry-After", [ref]$values)) { - $raw = @($values)[0] + $raw = @($values)[0]; if ($raw -match '^\d+$') { - $retryAfterSeconds = [int]$raw + $retryAfterSeconds = [int]$raw; } else { - $retryDate = [DateTimeOffset]::Parse($raw) - $seconds = [Math]::Ceiling(($retryDate - [DateTimeOffset]::UtcNow).TotalSeconds) - if ($seconds -gt 0) { - $retryAfterSeconds = [int]$seconds - } + $retryDate = [DateTimeOffset]::Parse($raw); + $seconds = [Math]::Ceiling(($retryDate - [DateTimeOffset]::UtcNow).TotalSeconds); + if ($seconds -gt 0) { $retryAfterSeconds = [int]$seconds; } } } } catch {} - # WebResponse-artige Header if ($null -eq $retryAfterSeconds) { try { - $raw = $headers["Retry-After"] + $raw = $headers["Retry-After"]; if ($raw) { if ($raw -match '^\d+$') { - $retryAfterSeconds = [int]$raw + $retryAfterSeconds = [int]$raw; } else { - $retryDate = [DateTimeOffset]::Parse($raw) - $seconds = [Math]::Ceiling(($retryDate - [DateTimeOffset]::UtcNow).TotalSeconds) - if ($seconds -gt 0) { - $retryAfterSeconds = [int]$seconds - } + $retryDate = [DateTimeOffset]::Parse($raw); + $seconds = [Math]::Ceiling(($retryDate - [DateTimeOffset]::UtcNow).TotalSeconds); + if ($seconds -gt 0) { $retryAfterSeconds = [int]$seconds; } } } } catch {} @@ -414,502 +342,317 @@ function Get-GraphFailureInfo { } } catch {} - # Fallback: Statuscode aus Text extrahieren, falls das Graph-Modul ihn nur dort liefert. if ($null -eq $statusCode) { - $combined = "$message $($ErrorRecord | Out-String)" + $msgOut = $ErrorRecord | Out-String; + $combined = "$message $msgOut"; if ($combined -match '(?"; + $result.Add("Message-ID: $messageId"); } - $date = [DateTimeOffset]::Now.ToString("ddd, dd MMM yyyy HH:mm:ss zzz", [Globalization.CultureInfo]::InvariantCulture) - $received = "Received: from [$RemoteAddress] by $hostname with SMTPGraphRelay; $date" + for ($i = 0; $i -lt$Lines.Count; $i++) {$result.Add($Lines[$i]); } + if ($separatorIndex -lt 0) {$result.Add(""); } - # Received immer ganz oben vor die urspruenglichen Header setzen. - $result.Add($received) - - if (-not $hasMessageId) { - $safeHost = ($hostname -replace '[^A-Za-z0-9._-]', '-') - $messageId = "<$QueueId.$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))@$safeHost>" - $result.Add("Message-ID: $messageId") - } - - for ($i = 0; $i -lt $Lines.Count; $i++) { - $result.Add($Lines[$i]) - } - - if ($separatorIndex -lt 0) { - $result.Add("") - } - - return $result + return $result; } - function Test-AddressInList { param( [Parameter(Mandatory)][System.Net.IPAddress]$Address, [object[]]$Entries - ) + ); - foreach ($entry in @($Entries)) { - if ($null -eq $entry) { continue } - - $value = [string]$entry - if ([string]::IsNullOrWhiteSpace($value)) { continue } - - if ($value -eq "*") { return $true } + $list = @($Entries); + foreach ($entry in$list) { + if ($null -eq$entry) { continue; } + $value = [string]$entry; + if ([string]::IsNullOrWhiteSpace($value)) { continue; } + if ($value -eq "*") { return $true; } try { if ($value -match '/') { - if (Test-IPv4InCidr -Address $Address -Cidr $value) { - return $true - } + if (Test-IPv4InCidr -Address ($Address) -Cidr ($value)) { return$true; } + } elseif ([System.Net.IPAddress]::Parse($value).Equals($Address)) { + return $true; } - elseif ([System.Net.IPAddress]::Parse($value).Equals($Address)) { - return $true - } - } - catch {} + } catch {} } - - return $false + return $false; } function Get-SmtpAuthSettings { - $requireAuth = $false - $maxFailures = 5 - $allowUnauthenticatedNetworks = @() - $users = @() + $requireAuth =$false; + $maxFailures = 5; + $allowUnauthenticatedNetworks = @();$users = @(); try { - if ($script:Config.Smtp.PSObject.Properties.Name -contains "RequireAuth") { - $requireAuth = [bool]$script:Config.Smtp.RequireAuth - } - + if ($script:Config.Smtp.PSObject.Properties.Name -contains "RequireAuth") { $requireAuth = [bool]$script:Config.Smtp.RequireAuth; } if ($script:Config.Smtp.PSObject.Properties.Name -contains "AuthMaxFailures") { - $v = [int]$script:Config.Smtp.AuthMaxFailures - if ($v -ge 1 -and $v -le 100) { - $maxFailures = $v - } + $v = [int]$script:Config.Smtp.AuthMaxFailures; + if ($v -ge 1 -and$v -le 100) { $maxFailures =$v; } } + if ($script:Config.Smtp.PSObject.Properties.Name -contains "AllowUnauthenticatedNetworks") { $allowUnauthenticatedNetworks = @($script:Config.Smtp.AllowUnauthenticatedNetworks); } + if ($script:Config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { $users = @($script:Config.Smtp.AuthUsers); } + } catch {} - if ($script:Config.Smtp.PSObject.Properties.Name -contains "AllowUnauthenticatedNetworks") { - $allowUnauthenticatedNetworks = @($script:Config.Smtp.AllowUnauthenticatedNetworks) - } - - if ($script:Config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { - $users = @($script:Config.Smtp.AuthUsers) - } - } - catch {} - - return [pscustomobject]@{ - RequireAuth = $requireAuth - AuthMaxFailures = $maxFailures - AllowUnauthenticatedNetworks = $allowUnauthenticatedNetworks - Users = $users - } + return [pscustomobject]@{ RequireAuth = $requireAuth; AuthMaxFailures =$maxFailures; AllowUnauthenticatedNetworks = $allowUnauthenticatedNetworks; Users =$users; }; } function Test-SmtpAuthenticationRequired { - param([Parameter(Mandatory)][System.Net.IPAddress]$Address) + param([Parameter(Mandatory)][System.Net.IPAddress]$Address); - $settings = Get-SmtpAuthSettings - - if (-not $settings.RequireAuth) { - return $false - } - - if (Test-AddressInList -Address $Address -Entries $settings.AllowUnauthenticatedNetworks) { - return $false - } - - return $true + $settings = Get-SmtpAuthSettings; + if (-not $settings.RequireAuth) { return$false; } + if (Test-AddressInList -Address ($Address) -Entries ($settings.AllowUnauthenticatedNetworks)) { return$false; } + return $true; } function ConvertFrom-Base64Utf8 { - param([Parameter(Mandatory)][string]$Value) - + param([Parameter(Mandatory)][string]$Value); try { - $bytes = [Convert]::FromBase64String($Value) - return [Text.Encoding]::UTF8.GetString($bytes) - } - catch { - throw "Invalid Base64 data" - } + $bytes = [Convert]::FromBase64String($Value); + return [Text.Encoding]::UTF8.GetString($bytes); + } catch { throw "Invalid Base64 data"; } } function Invoke-Pbkdf2Sha256 { @@ -918,1293 +661,786 @@ function Invoke-Pbkdf2Sha256 { [Parameter(Mandatory)][byte[]]$Salt, [Parameter(Mandatory)][int]$Iterations, [int]$Length = 32 - ) + ); - if ($Iterations -lt 1) { - throw "Iterations must be greater than zero." - } + if ($Iterations -lt 1) { throw "Iterations must be greater than zero."; } - # Auf unterstuetzten .NET-Framework-Versionen verwenden wir die native, - # schnelle PBKDF2-SHA256-Implementierung. try { - $derive = New-Object System.Security.Cryptography.Rfc2898DeriveBytes( - $Password, - $Salt, - $Iterations, - [System.Security.Cryptography.HashAlgorithmName]::SHA256 - ) - + $derive = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Password, $Salt,$Iterations, [System.Security.Cryptography.HashAlgorithmName]::SHA256); + try { return $derive.GetBytes($Length); } finally {$derive.Dispose(); } + } catch { + $hmac = New-Object System.Security.Cryptography.HMACSHA256; + $hmac.Key = [Text.Encoding]::UTF8.GetBytes($Password); try { - return $derive.GetBytes($Length) - } - finally { - $derive.Dispose() - } - } - catch { - # Kompatibilitaets-Fallback fuer aeltere .NET-Framework-Staende. - $hmac = New-Object System.Security.Cryptography.HMACSHA256 - $hmac.Key = [Text.Encoding]::UTF8.GetBytes($Password) - - try { - $hashLength = 32 - $blocks = [Math]::Ceiling($Length / [double]$hashLength) - $output = New-Object byte[] ($blocks * $hashLength) - $offset = 0 + $hashLength = 32; + $blocks = [Math]::Ceiling($Length / [double]$hashLength); + $output = New-Object byte[] ($blocks * $hashLength);$offset = 0; for ($block = 1; $block -le $blocks; $block++) { - $blockBytes = [BitConverter]::GetBytes([int]$block) - if ([BitConverter]::IsLittleEndian) { - [Array]::Reverse($blockBytes) + $blockBytes = [BitConverter]::GetBytes([int]$block); + if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($blockBytes); } + + $input = New-Object byte[] ($Salt.Length + 4); + [Array]::Copy($Salt, 0, $input, 0,$Salt.Length); + [Array]::Copy($blockBytes, 0, $input,$Salt.Length, 4); + + $u = $hmac.ComputeHash($input); + $t = New-Object byte[]$u.Length; + [Array]::Copy($u, $t,$u.Length); + + for ($i = 2; $i -le$Iterations; $i++) {$u = $hmac.ComputeHash($u); + for ($j = 0; $j -lt $t.Length; $j++) { $t[$j] = $t[$j] -bxor $u[$j]; } } - $input = New-Object byte[] ($Salt.Length + 4) - [Array]::Copy($Salt, 0, $input, 0, $Salt.Length) - [Array]::Copy($blockBytes, 0, $input, $Salt.Length, 4) - - $u = $hmac.ComputeHash($input) - $t = New-Object byte[] $u.Length - [Array]::Copy($u, $t, $u.Length) - - for ($i = 2; $i -le $Iterations; $i++) { - $u = $hmac.ComputeHash($u) - for ($j = 0; $j -lt $t.Length; $j++) { - $t[$j] = $t[$j] -bxor $u[$j] - } - } - - [Array]::Copy($t, 0, $output, $offset, $t.Length) - $offset += $t.Length + [Array]::Copy($t, 0,$output, $offset,$t.Length); + $offset +=$t.Length; } - $result = New-Object byte[] $Length - [Array]::Copy($output, 0, $result, 0, $Length) - return $result - } - finally { - $hmac.Dispose() - } + $result = New-Object byte[]$Length; + [Array]::Copy($output, 0, $result, 0,$Length); + return $result; + } finally { $hmac.Dispose(); } } } function Test-FixedTimeEquals { - param( - [Parameter(Mandatory)][byte[]]$A, - [Parameter(Mandatory)][byte[]]$B - ) - - if ($A.Length -ne $B.Length) { - return $false - } - - [int]$difference = 0 - for ($i = 0; $i -lt $A.Length; $i++) { - $difference = $difference -bor ($A[$i] -bxor $B[$i]) - } - - return ($difference -eq 0) + param([Parameter(Mandatory)][byte[]]$A, [Parameter(Mandatory)][byte[]]$B); + if ($A.Length -ne $B.Length) { return$false; } + [int]$difference = 0; + for ($i = 0; $i -lt $A.Length; $i++) { $difference =$difference -bor ($A[$i] -bxor $B[$i]); } + return ($difference -eq 0); } function Test-SmtpCredentials { - param( - [Parameter(Mandatory)][string]$Username, - [Parameter(Mandatory)][string]$Password - ) - - if ([string]::IsNullOrWhiteSpace($Username)) { - return $false - } - - $settings = Get-SmtpAuthSettings - $user = $settings.Users | - Where-Object { ([string]$_.Username).Equals($Username, [StringComparison]::OrdinalIgnoreCase) } | - Select-Object -First 1 - - if (-not $user) { - # Bewusst keine Unterscheidung im SMTP-Result zwischen unbekanntem - # Benutzer und falschem Passwort. - return $false - } + param([Parameter(Mandatory)][string]$Username, [Parameter(Mandatory)][string]$Password); + if ([string]::IsNullOrWhiteSpace($Username)) { return $false; }$settings = Get-SmtpAuthSettings; + $user =$settings.Users | Where-Object { ([string]$_.Username).Equals($Username, [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1; + if (-not $user) { return$false; } try { - $salt = [Convert]::FromBase64String([string]$user.Salt) - $expected = [Convert]::FromBase64String([string]$user.PasswordHash) - $iterations = [int]$user.Iterations + $salt = [Convert]::FromBase64String([string]$user.Salt); + $expected = [Convert]::FromBase64String([string]$user.PasswordHash); + $iterations = [int]$user.Iterations; - if ($iterations -lt 10000 -or $expected.Length -lt 16 -or $salt.Length -lt 8) { - return $false - } + if ($iterations -lt 10000 -or$expected.Length -lt 16 -or $salt.Length -lt 8) { return$false; } - $actual = Invoke-Pbkdf2Sha256 ` - -Password $Password ` - -Salt $salt ` - -Iterations $iterations ` - -Length $expected.Length - - return (Test-FixedTimeEquals -A $actual -B$expected) - } - catch { - return $false - } + $actual = Invoke-Pbkdf2Sha256 -Password ($Password) -Salt ($salt) -Iterations ($iterations) -Length ($expected.Length); + return (Test-FixedTimeEquals -A ($actual) -B ($expected)); + } catch { return $false; } } function Invoke-SmtpAuthLogin { - param( - [Parameter(Mandatory)][System.IO.StreamReader]$Reader, - [Parameter(Mandatory)][System.IO.StreamWriter]$Writer, - [string]$InitialResponse - ) - + param([Parameter(Mandatory)][System.IO.StreamReader]$Reader, [Parameter(Mandatory)][System.IO.StreamWriter]$Writer, [string]$InitialResponse); try { if ([string]::IsNullOrWhiteSpace($InitialResponse)) { - Write-SmtpLine $Writer "334 VXNlcm5hbWU6" - $encodedUser =$Reader.ReadLine() - if ($null -eq$encodedUser) { throw "Client disconnected during AUTH LOGIN" } - } - else { - $encodedUser =$InitialResponse - } + Write-SmtpLine -Writer ($Writer) -Line "334 VXNlcm5hbWU6"; + $encodedUser =$Reader.ReadLine(); + if ($null -eq$encodedUser) { throw "Client disconnected during AUTH LOGIN"; } + } else { $encodedUser =$InitialResponse; } - $username = ConvertFrom-Base64Utf8 -Value$encodedUser + $username = ConvertFrom-Base64Utf8 -Value ($encodedUser); - Write-SmtpLine $Writer "334 UGFzc3dvcmQ6" - $encodedPassword =$Reader.ReadLine() - if ($null -eq$encodedPassword) { throw "Client disconnected during AUTH LOGIN" } + Write-SmtpLine -Writer ($Writer) -Line "334 UGFzc3dvcmQ6"; + $encodedPassword =$Reader.ReadLine(); + if ($null -eq$encodedPassword) { throw "Client disconnected during AUTH LOGIN"; } - $password = ConvertFrom-Base64Utf8 -Value$encodedPassword - - return [pscustomobject]@{ - Valid = (Test-SmtpCredentials -Username $username -Password$password) - Username = $username - ProtocolError = $false - } - } - catch { - return [pscustomobject]@{ - Valid = $false - Username = $null - ProtocolError = $true - } - } + $password = ConvertFrom-Base64Utf8 -Value ($encodedPassword);$isValid = Test-SmtpCredentials -Username ($username) -Password ($password); + return [pscustomobject]@{ Valid = $isValid; Username = $username; ProtocolError =$false; }; + } catch { return [pscustomobject]@{ Valid = $false; Username = $null; ProtocolError =$true; }; } } function Invoke-SmtpAuthPlain { - param( - [Parameter(Mandatory)][System.IO.StreamReader]$Reader, - [Parameter(Mandatory)][System.IO.StreamWriter]$Writer, - [string]$InitialResponse - ) - + param([Parameter(Mandatory)][System.IO.StreamReader]$Reader, [Parameter(Mandatory)][System.IO.StreamWriter]$Writer, [string]$InitialResponse); try { - $encoded =$InitialResponse - + $encoded =$InitialResponse; if ([string]::IsNullOrWhiteSpace($encoded)) { - Write-SmtpLine $Writer "334" - $encoded =$Reader.ReadLine() - if ($null -eq$encoded) { throw "Client disconnected during AUTH PLAIN" } + Write-SmtpLine -Writer ($Writer) -Line "334"; + $encoded =$Reader.ReadLine(); + if ($null -eq$encoded) { throw "Client disconnected during AUTH PLAIN"; } } - $decoded = ConvertFrom-Base64Utf8 -Value$encoded - $parts =$decoded -split "`0", 3 + $decoded = ConvertFrom-Base64Utf8 -Value ($encoded); + $parts =$decoded -split "`0", 3; + if ($parts.Count -lt 3) { throw "Invalid AUTH PLAIN payload"; } - if ($parts.Count -lt 3) { - throw "Invalid AUTH PLAIN payload" - } - - # RFC 4616: [authzid] NUL authcid NUL passwd - $username = $parts[1] - $password = $parts[2] - - return [pscustomobject]@{ - Valid = (Test-SmtpCredentials -Username $username -Password $password) - Username = $username - ProtocolError = $false - } - } - catch { - return [pscustomobject]@{ - Valid = $false - Username = $null - ProtocolError = $true - } - } + $username = $parts[1]; + $password = $parts[2]; + $isValid = Test-SmtpCredentials -Username ($username) -Password ($password); + return [pscustomobject]@{ Valid = $isValid; Username = $username; ProtocolError = $false; }; + } catch { return [pscustomobject]@{ Valid = $false; Username = $null; ProtocolError = $true; }; } } - function Get-DebugSettings { - $defaults = [ordered]@{ - Enabled = $false - DelayBeforeGraphSendSeconds = 0 - SimulateGraphStatus = 0 - SimulateQueueWriteFailure = $false - OverridePendingCount = -1 - OverrideFreeDiskSpaceMB = -1 - } - + $defaults = [ordered]@{ Enabled = $false; DelayBeforeGraphSendSeconds = 0; SimulateGraphStatus = 0; SimulateQueueWriteFailure = $false; OverridePendingCount = -1; OverrideFreeDiskSpaceMB = -1; }; try { - if (-not ($script:Config.PSObject.Properties.Name -contains "Debug") -or -not $script:Config.Debug) { - return [pscustomobject]$defaults - } - - $d = $script:Config.Debug - - if ($d.PSObject.Properties.Name -contains "Enabled") { - $defaults.Enabled = [bool]$d.Enabled - } - + if (-not ($script:Config.PSObject.Properties.Name -contains "Debug") -or -not $script:Config.Debug) { return [pscustomobject]$defaults; } + $d = $script:Config.Debug; + if ($d.PSObject.Properties.Name -contains "Enabled") { $defaults.Enabled = [bool]$d.Enabled; } if ($d.PSObject.Properties.Name -contains "DelayBeforeGraphSendSeconds") { - $v = [int]$d.DelayBeforeGraphSendSeconds - if ($v -ge 0 -and $v -le 300) { $defaults.DelayBeforeGraphSendSeconds = $v } + $v = [int]$d.DelayBeforeGraphSendSeconds; + if ($v -ge 0 -and $v -le 300) { $defaults.DelayBeforeGraphSendSeconds = $v; } } - if ($d.PSObject.Properties.Name -contains "SimulateGraphStatus") { - $v = [int]$d.SimulateGraphStatus - if ($v -eq 0 -or ($v -ge 400 -and $v -le 599)) { $defaults.SimulateGraphStatus = $v } + $v = [int]$d.SimulateGraphStatus; + if ($v -eq 0 -or ($v -ge 400 -and $v -le 599)) { $defaults.SimulateGraphStatus = $v; } } - - if ($d.PSObject.Properties.Name -contains "SimulateQueueWriteFailure") { - $defaults.SimulateQueueWriteFailure = [bool]$d.SimulateQueueWriteFailure - } - + if ($d.PSObject.Properties.Name -contains "SimulateQueueWriteFailure") { $defaults.SimulateQueueWriteFailure = [bool]$d.SimulateQueueWriteFailure; } if ($d.PSObject.Properties.Name -contains "OverridePendingCount") { - $v = [int]$d.OverridePendingCount - if ($v -ge -1 -and $v -le 1000000) { $defaults.OverridePendingCount = $v } + $v = [int]$d.OverridePendingCount; + if ($v -ge -1 -and $v -le 1000000) { $defaults.OverridePendingCount = $v; } } - if ($d.PSObject.Properties.Name -contains "OverrideFreeDiskSpaceMB") { - $v = [int]$d.OverrideFreeDiskSpaceMB - if ($v -ge -1 -and $v -le 1048576) { $defaults.OverrideFreeDiskSpaceMB = $v } + $v = [int]$d.OverrideFreeDiskSpaceMB; + if ($v -ge -1 -and $v -le 1048576) { $defaults.OverrideFreeDiskSpaceMB = $v; } } - } - catch {} - - return [pscustomobject]$defaults + } catch {} + return [pscustomobject]$defaults; } function Get-SmtpLimits { - $maxRecipients = 50 - $maxMessagesPerConnection = 25 - $maxPendingMessages = 5000 - $minFreeDiskSpaceMB = 1024 + $maxRecipients = 50; + $maxMessagesPerConnection = 25; + $maxPendingMessages = 5000; + $minFreeDiskSpaceMB = 1024; try { if ($script:Config.Smtp.PSObject.Properties.Name -contains "MaxRecipients") { - $v = [int]$script:Config.Smtp.MaxRecipients - if ($v -ge 1 -and $v -le 1000) { $maxRecipients = $v } + $v = [int]$script:Config.Smtp.MaxRecipients; + if ($v -ge 1 -and $v -le 1000) { $maxRecipients = $v; } } - if ($script:Config.Smtp.PSObject.Properties.Name -contains "MaxMessagesPerConnection") { - $v = [int]$script:Config.Smtp.MaxMessagesPerConnection - if ($v -ge 1 -and $v -le 10000) { $maxMessagesPerConnection = $v } + $v = [int]$script:Config.Smtp.MaxMessagesPerConnection; + if ($v -ge 1 -and $v -le 10000) { $maxMessagesPerConnection = $v; } } - if ($script:Config.Queue.PSObject.Properties.Name -contains "MaxPendingMessages") { - $v = [int]$script:Config.Queue.MaxPendingMessages - if ($v -ge 1 -and $v -le 1000000) { $maxPendingMessages = $v } + $v = [int]$script:Config.Queue.MaxPendingMessages; + if ($v -ge 1 -and $v -le 1000000) { $maxPendingMessages = $v; } } - if ($script:Config.Queue.PSObject.Properties.Name -contains "MinFreeDiskSpaceMB") { - $v = [int]$script:Config.Queue.MinFreeDiskSpaceMB - if ($v -ge 0 -and $v -le 1048576) { $minFreeDiskSpaceMB = $v } + $v = [int]$script:Config.Queue.MinFreeDiskSpaceMB; + if ($v -ge 0 -and $v -le 1048576) { $minFreeDiskSpaceMB = $v; } } - } - catch {} + } catch {} - return [pscustomobject]@{ - MaxRecipients = $maxRecipients - MaxMessagesPerConnection = $maxMessagesPerConnection - MaxPendingMessages = $maxPendingMessages - MinFreeDiskSpaceMB = $minFreeDiskSpaceMB - } + return [pscustomobject]@{ MaxRecipients = $maxRecipients; MaxMessagesPerConnection = $maxMessagesPerConnection; MaxPendingMessages = $maxPendingMessages; MinFreeDiskSpaceMB = $minFreeDiskSpaceMB; }; } function Get-QueuePressure { - $dirs = Get-QueueDirectories - $limits = Get-SmtpLimits - - $debug = Get-DebugSettings + $dirs = Get-QueueDirectories; + $limits = Get-SmtpLimits; + $debug = Get-DebugSettings; try { if ($debug.Enabled -and $debug.OverridePendingCount -ge 0) { - $pendingCount = [int]$debug.OverridePendingCount - Write-Log ("QA: PendingCount wird mit {0} simuliert." -f $pendingCount) "WARN" - } - else { - $pendingCount = @( - Get-ChildItem -LiteralPath $dirs.Pending -Filter "*.eml" -File -ErrorAction Stop - ).Count - } - } - catch { - return [pscustomobject]@{ - Accept = $false - SmtpCode = "451 4.3.0 SMTPGraphRelay queue temporarily unavailable" - Reason = "Pending-Queue konnte nicht gelesen werden: $($_.Exception.Message)" - PendingCount = $null - FreeSpaceMB = $null + $pendingCount = [int]$debug.OverridePendingCount; + Write-Log ("QA: PendingCount wird mit {0} simuliert." -f $pendingCount) "WARN"; + } else { + $pendingFiles = Get-ChildItem -LiteralPath ($dirs.Pending) -Filter "*.eml" -File -ErrorAction Stop; + $pendingCount = @($pendingFiles).Count; } + } catch { + return [pscustomobject]@{ Accept = $false; SmtpCode = "451 4.3.0 SMTPGraphRelay queue temporarily unavailable"; Reason = "Pending-Queue konnte nicht gelesen werden: $($_.Exception.Message)"; PendingCount = $null; FreeSpaceMB = $null; }; } if ($pendingCount -ge $limits.MaxPendingMessages) { - return [pscustomobject]@{ - Accept = $false - SmtpCode = "452 4.3.1 SMTPGraphRelay queue limit reached" - Reason = "Pending-Queue-Limit erreicht ($pendingCount/$($limits.MaxPendingMessages))" - PendingCount = $pendingCount - FreeSpaceMB = $null - } + return [pscustomobject]@{ Accept = $false; SmtpCode = "452 4.3.1 SMTPGraphRelay queue limit reached"; Reason = "Pending-Queue-Limit erreicht ($pendingCount/$($limits.MaxPendingMessages))"; PendingCount = $pendingCount; FreeSpaceMB = $null; }; } - $freeSpaceMB = $null - + $freeSpaceMB = $null; try { - $fullQueuePath = [IO.Path]::GetFullPath($dirs.Root) - $root = [IO.Path]::GetPathRoot($fullQueuePath) - + $fullQueuePath = [IO.Path]::GetFullPath($dirs.Root); + $root = [IO.Path]::GetPathRoot($fullQueuePath); if ($root) { - $driveInfo = New-Object System.IO.DriveInfo($root) + $driveInfo = New-Object System.IO.DriveInfo($root); if ($driveInfo.IsReady) { if ($debug.Enabled -and $debug.OverrideFreeDiskSpaceMB -ge 0) { - $freeSpaceMB = [int]$debug.OverrideFreeDiskSpaceMB - Write-Log ("QA: freier Speicher wird mit {0} MB simuliert." -f $freeSpaceMB) "WARN" - } - else { - $freeSpaceMB = [Math]::Floor($driveInfo.AvailableFreeSpace / 1MB) + $freeSpaceMB = [int]$debug.OverrideFreeDiskSpaceMB; + Write-Log ("QA: freier Speicher wird mit {0} MB simuliert." -f $freeSpaceMB) "WARN"; + } else { + $freeSpaceMB = [Math]::Floor($driveInfo.AvailableFreeSpace / 1MB); } } } - } - catch { - return [pscustomobject]@{ - Accept = $false - SmtpCode = "451 4.3.0 SMTPGraphRelay storage status unavailable" - Reason = "Freier Speicher konnte nicht ermittelt werden: $($_.Exception.Message)" - PendingCount = $pendingCount - FreeSpaceMB = $null - } + } catch { + return [pscustomobject]@{ Accept = $false; SmtpCode = "451 4.3.0 SMTPGraphRelay storage status unavailable"; Reason = "Freier Speicher konnte nicht ermittelt werden: $($_.Exception.Message)"; PendingCount = $pendingCount; FreeSpaceMB = $null; }; } if ($null -ne $freeSpaceMB -and $freeSpaceMB -lt $limits.MinFreeDiskSpaceMB) { - return [pscustomobject]@{ - Accept = $false - SmtpCode = "452 4.3.1 Insufficient system storage" - Reason = "Freier Speicher zu niedrig (${freeSpaceMB}MB < $($limits.MinFreeDiskSpaceMB)MB)" - PendingCount = $pendingCount - FreeSpaceMB = $freeSpaceMB - } + return [pscustomobject]@{ Accept = $false; SmtpCode = "452 4.3.1 Insufficient system storage"; Reason = "Freier Speicher zu niedrig (${freeSpaceMB}MB < $($limits.MinFreeDiskSpaceMB)MB)"; PendingCount = $pendingCount; FreeSpaceMB = $freeSpaceMB; }; } - return [pscustomobject]@{ - Accept = $true - SmtpCode = $null - Reason = "OK" - PendingCount = $pendingCount - FreeSpaceMB = $freeSpaceMB - } + return [pscustomobject]@{ Accept = $true; SmtpCode = $null; Reason = "OK"; PendingCount = $pendingCount; FreeSpaceMB = $freeSpaceMB; }; } function Save-SmtpMessage { param( - [Parameter(Mandatory)] - [AllowEmptyCollection()] - [AllowEmptyString()] - [System.Collections.Generic.List[string]]$Lines, - - [Parameter(Mandatory)] - [string]$MailFrom, - - [Parameter(Mandatory)] - [string[]]$Recipients, - - [Parameter(Mandatory)] - [string]$RemoteAddress, - + [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][System.Collections.Generic.List[string]]$Lines, + [Parameter(Mandatory)][string]$MailFrom, + [Parameter(Mandatory)][string[]]$Recipients, + [Parameter(Mandatory)][string]$RemoteAddress, [string]$AuthenticatedUser, - [string]$QueueId - ) + ); - $dirs = Get-QueueDirectories + $dirs = Get-QueueDirectories; + if ([string]::IsNullOrWhiteSpace($QueueId)) { $QueueId = New-QueueId; } - if ([string]::IsNullOrWhiteSpace($QueueId)) { - $QueueId = New-QueueId - } + $incomingEmlTmp = Join-Path ($dirs.Incoming) "$QueueId.eml.tmp"; + $incomingMetaTmp = Join-Path ($dirs.Incoming) "$QueueId.eml.json.tmp"; + $pendingEml = Join-Path ($dirs.Pending) "$QueueId.eml"; + $pendingMeta = "$pendingEml.json"; - $incomingEmlTmp = Join-Path $dirs.Incoming "$QueueId.eml.tmp" - $incomingMetaTmp = Join-Path $dirs.Incoming "$QueueId.eml.json.tmp" - - $pendingEml = Join-Path $dirs.Pending "$QueueId.eml" - $pendingMeta = "$pendingEml.json" - - $messageLines = Add-RelayMessageHeaders ` - -Lines $Lines ` - -QueueId $QueueId ` - -RemoteAddress $RemoteAddress - - # SMTP DATA wird in CRLF normalisiert. - $raw = ($messageLines -join "`r`n") + "`r`n" + $messageLines = Add-RelayMessageHeaders -Lines ($Lines) -QueueId ($QueueId) -RemoteAddress ($RemoteAddress); + $raw = ($messageLines -join "`r`n") + "`r`n"; - # --------------------------------------------------------- - # BUGFIX: ISO-8859-1 (Latin1) Byte-Passthrough verwenden - # --------------------------------------------------------- - $latin1 = [System.Text.Encoding]::GetEncoding(28591) - - $meta = [ordered]@{ - QueueId = $QueueId - ReceivedUtc = [DateTime]::UtcNow.ToString("o") - RemoteAddress = $RemoteAddress - AuthenticatedUser = $AuthenticatedUser - EnvelopeFrom = $MailFrom - EnvelopeRecipients = @($Recipients) - RetryCount = 0 - NextAttemptUtc = [DateTime]::UtcNow.ToString("o") - } + $latin1 = [System.Text.Encoding]::GetEncoding(28591); + $meta = [ordered]@{ QueueId = $QueueId; ReceivedUtc = [DateTime]::UtcNow.ToString("o"); RemoteAddress = $RemoteAddress; AuthenticatedUser = $AuthenticatedUser; EnvelopeFrom = $MailFrom; EnvelopeRecipients = @($Recipients); RetryCount = 0; NextAttemptUtc = [DateTime]::UtcNow.ToString("o"); }; try { - $debug = Get-DebugSettings - if ($debug.Enabled -and$debug.SimulateQueueWriteFailure) { - Write-Log ("[{0}] QA: simulierter Queue-Schreibfehler." -f $QueueId) "WARN" - throw "QA simulated queue write failure" + $debug = Get-DebugSettings; + if ($debug.Enabled -and $debug.SimulateQueueWriteFailure) { + Write-Log ("[{0}] QA: simulierter Queue-Schreibfehler." -f $QueueId) "WARN"; + throw "QA simulated queue write failure"; } - - # Erst vollstaendig in incoming schreiben (als Latin1 um alle 8-Bit Zeichen 1:1 zu behalten). - [IO.File]::WriteAllText($incomingEmlTmp, $raw,$latin1) - - $meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath$incomingMetaTmp -Encoding UTF8 - - # Metadaten zuerst finalisieren, Maildatei zuletzt. - # Erst wenn die .eml in pending liegt, ist sie fuer den Worker sichtbar. - Move-Item -LiteralPath $incomingMetaTmp -Destination$pendingMeta -Force - Move-Item -LiteralPath $incomingEmlTmp -Destination$pendingEml -Force - } - catch { - Remove-Item -LiteralPath $incomingEmlTmp -Force -ErrorAction SilentlyContinue - Remove-Item -LiteralPath $incomingMetaTmp -Force -ErrorAction SilentlyContinue - Remove-Item -LiteralPath $pendingMeta -Force -ErrorAction SilentlyContinue - Remove-Item -LiteralPath $pendingEml -Force -ErrorAction SilentlyContinue - throw + [IO.File]::WriteAllText($incomingEmlTmp, $raw, $latin1); + $meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath ($incomingMetaTmp) -Encoding UTF8; + Move-Item -LiteralPath ($incomingMetaTmp) -Destination ($pendingMeta) -Force; + Move-Item -LiteralPath ($incomingEmlTmp) -Destination ($pendingEml) -Force; + } catch { + Remove-Item -LiteralPath ($incomingEmlTmp) -Force -ErrorAction SilentlyContinue; + Remove-Item -LiteralPath ($incomingMetaTmp) -Force -ErrorAction SilentlyContinue; + Remove-Item -LiteralPath ($pendingMeta) -Force -ErrorAction SilentlyContinue; + Remove-Item -LiteralPath ($pendingEml) -Force -ErrorAction SilentlyContinue; + throw; } - $authText = if ([string]::IsNullOrWhiteSpace($AuthenticatedUser)) { "unauthenticated" } else { $AuthenticatedUser } - Write-Log "[$QueueId] Mail angenommen | Von=$MailFrom | An=$($Recipients -join ', ') | Client=$RemoteAddress | Auth=$authText" - return [pscustomobject]@{ - QueueId = $QueueId - Path = $pendingEml - } + $authText = if ([string]::IsNullOrWhiteSpace($AuthenticatedUser)) { "unauthenticated"; } else { $AuthenticatedUser; }; + $rcptStr = $Recipients -join ', '; + Write-Log "[$QueueId] Mail angenommen | Von=$MailFrom | An=$rcptStr | Client=$RemoteAddress | Auth=$authText"; + return [pscustomobject]@{ QueueId = $QueueId; Path = $pendingEml; }; } function Write-SmtpLine { - param( - [Parameter(Mandatory)][System.IO.StreamWriter]$Writer, - [Parameter(Mandatory)][string]$Line - ) - $Writer.WriteLine($Line); $Writer.Flush(); + param([Parameter(Mandatory)][System.IO.StreamWriter]$Writer, [Parameter(Mandatory)][string]$Line); + $Writer.WriteLine($Line); + $Writer.Flush(); } function Handle-SmtpClient { - param([Parameter(Mandatory)][System.Net.Sockets.TcpClient]$Client) + param([Parameter(Mandatory)][System.Net.Sockets.TcpClient]$Client); - $remote =$Client.Client.RemoteEndPoint - $remoteIp = ([System.Net.IPEndPoint]$remote).Address - Write-Log "SMTP-Verbindung von $remoteIp" + $remote = $Client.Client.RemoteEndPoint; + $remoteIp = ([System.Net.IPEndPoint]$remote).Address; + Write-Log "SMTP-Verbindung von $remoteIp"; - if (-not (Test-ClientAllowed -Address $remoteIp)) { + if (-not (Test-ClientAllowed -Address ($remoteIp))) { try { - $stream = $Client.GetStream(); $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII); $writer.NewLine = "`r`n" - $writer.AutoFlush =$true - Write-SmtpLine $writer "554 5.7.1 Client not allowed" + $stream = $Client.GetStream(); + $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII); + $writer.NewLine = "`r`n"; + $writer.AutoFlush = $true; + Write-SmtpLine -Writer ($writer) -Line "554 5.7.1 Client not allowed"; } catch {} - - $Client.Close() - Write-Log "Client abgewiesen: $remoteIp" "WARN" - return + $Client.Close(); + Write-Log "Client abgewiesen: $remoteIp" "WARN"; + return; } - $stream =$Client.GetStream() - $stream.ReadTimeout = [int]$script:Config.Smtp.ClientTimeoutSeconds * 1000 + $stream = $Client.GetStream(); + $stream.ReadTimeout = [int]$script:Config.Smtp.ClientTimeoutSeconds * 1000; + $latin1 = [System.Text.Encoding]::GetEncoding(28591); + + $reader = New-Object System.IO.StreamReader($stream, $latin1, $false, 4096, $true); + $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII, 4096, $true); + $writer.NewLine = "`r`n"; + $writer.AutoFlush = $true; - # --------------------------------------------------------- - # BUGFIX: Scanner senden oft ISO-8859-1. Wir nutzen Codepage - # 28591 (Latin1) anstatt UTF-8 um eingehende Bytes 1:1 durchzureichen. - # --------------------------------------------------------- - $latin1 = [System.Text.Encoding]::GetEncoding(28591); $reader = New-Object System.IO.StreamReader( - $stream,$latin1, - $false, 4096,$true - ) + $mailFrom = $null; + $recipients = New-Object System.Collections.Generic.List[string]; + $acceptedMessages = 0; + $limits = Get-SmtpLimits; + $authSettings = Get-SmtpAuthSettings; - $writer = New-Object System.IO.StreamWriter($stream, - [System.Text.Encoding]::ASCII, - 4096, - $true - ) + $authenticated = $false; + $authenticatedUser = $null; + $authFailures = 0; + $authLocked = $false; + $authRequired = Test-SmtpAuthenticationRequired -Address ($remoteIp); + $usersCount = @($authSettings.Users).Count; + $authAvailable = ($usersCount -gt 0); - $writer.NewLine = "`r`n" - $writer.AutoFlush =$true - - $mailFrom =$null - $recipients = New-Object System.Collections.Generic.List[string]$acceptedMessages = 0 - $limits = Get-SmtpLimits$authSettings = Get-SmtpAuthSettings - - $authenticated = $false$authenticatedUser = $null$authFailures = 0 - $authLocked =$false - $authRequired = Test-SmtpAuthenticationRequired -Address$remoteIp - $authAvailable = (@($authSettings.Users).Count -gt 0) - - Write-SmtpLine $writer ("220 {0} SMTPGraphRelay ready" -f $script:Config.Smtp.Hostname) + Write-SmtpLine -Writer ($writer) -Line ("220 {0} SMTPGraphRelay ready" -f $script:Config.Smtp.Hostname); try { while ($Client.Connected) { - $line =$reader.ReadLine() - if ($null -eq$line) { break } + $line = $reader.ReadLine(); + if ($null -eq $line) { break; } - if ($line -match '^(?i)EHLO\s+(.+); $') { - Write-SmtpLine $writer ("250-{0}" -f $script:Config.Smtp.Hostname) - Write-SmtpLine $writer ("250-SIZE {0}" -f ([int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024)) - - if ($authAvailable) { - Write-SmtpLine $writer "250-AUTH LOGIN PLAIN" - } - - Write-SmtpLine $writer "250 8BITMIME" + if ($line -match '^(?i)EHLO\s+(.+)$') { + Write-SmtpLine -Writer ($writer) -Line ("250-{0}" -f $script:Config.Smtp.Hostname); + $sz = [int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024; + Write-SmtpLine -Writer ($writer) -Line ("250-SIZE {0}" -f $sz); + if ($authAvailable) { Write-SmtpLine -Writer ($writer) -Line "250-AUTH LOGIN PLAIN"; } + Write-SmtpLine -Writer ($writer) -Line "250 8BITMIME"; } - elseif ($line -match '^(?i)HELO\s+(.+); $') { - Write-SmtpLine $writer ("250 {0}" -f $script:Config.Smtp.Hostname) + elseif ($line -match '^(?i)HELO\s+(.+)$') { + Write-SmtpLine -Writer ($writer) -Line ("250 {0}" -f $script:Config.Smtp.Hostname); } elseif ($line -match '^(?i)AUTH\s+LOGIN(?:\s+(\S+))?\s*$') { - if (-not $authAvailable) { - Write-SmtpLine $writer "504 5.7.4 Authentication mechanism unavailable" - continue + if (-not $authAvailable) { Write-SmtpLine -Writer ($writer) -Line "504 5.7.4 Authentication mechanism unavailable"; continue; } + if ($authenticated) { Write-SmtpLine -Writer ($writer) -Line "503 5.5.0 Already authenticated"; continue; } + if ($authLocked) { Write-SmtpLine -Writer ($writer) -Line "454 4.7.0 Too many authentication failures"; continue; } + + $initial = $matches[1]; + $result = Invoke-SmtpAuthLogin -Reader ($reader) -Writer ($writer) -InitialResponse ($initial); + + if ($result.ProtocolError) { + $authFailures++; + Write-SmtpLine -Writer ($writer) -Line "501 5.5.2 Invalid authentication data"; + } elseif ($result.Valid) { + $authenticated = $true; + $authenticatedUser = $result.Username; + $authFailures = 0; + Write-SmtpLine -Writer ($writer) -Line "235 2.7.0 Authentication successful"; + Write-Log ("SMTP-AUTH LOGIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp, $authenticatedUser); + continue; + } else { + $authFailures++; + Write-SmtpLine -Writer ($writer) -Line "535 5.7.8 Authentication credentials invalid"; + Write-Log ("SMTP-AUTH LOGIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp, $result.Username) "WARN"; } - if ($authenticated) { - Write-SmtpLine $writer "503 5.5.0 Already authenticated" - continue - } - - if ($authLocked) { - Write-SmtpLine $writer "454 4.7.0 Too many authentication failures" - continue - } - - $initial =$matches[1] - $result = Invoke-SmtpAuthLogin -Reader$reader -Writer $writer -InitialResponse$initial - - if ($result.ProtocolError) {$authFailures++ - Write-SmtpLine $writer "501 5.5.2 Invalid authentication data" - } - elseif ($result.Valid) { - $authenticated =$true - $authenticatedUser =$result.Username - $authFailures = 0 - Write-SmtpLine $writer "235 2.7.0 Authentication successful" - Write-Log ("SMTP-AUTH LOGIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp,$authenticatedUser) - continue - } - else { - $authFailures++ - Write-SmtpLine $writer "535 5.7.8 Authentication credentials invalid" - Write-Log ("SMTP-AUTH LOGIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp,$result.Username) "WARN" - } - - if ($authFailures -ge$authSettings.AuthMaxFailures) { - $authLocked =$true - Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures,$remoteIp) "WARN" + if ($authFailures -ge $authSettings.AuthMaxFailures) { + $authLocked = $true; + Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures, $remoteIp) "WARN"; } } elseif ($line -match '^(?i)AUTH\s+PLAIN(?:\s+(\S+))?\s*$') { - if (-not $authAvailable) { - Write-SmtpLine $writer "504 5.7.4 Authentication mechanism unavailable" - continue + if (-not $authAvailable) { Write-SmtpLine -Writer ($writer) -Line "504 5.7.4 Authentication mechanism unavailable"; continue; } + if ($authenticated) { Write-SmtpLine -Writer ($writer) -Line "503 5.5.0 Already authenticated"; continue; } + if ($authLocked) { Write-SmtpLine -Writer ($writer) -Line "454 4.7.0 Too many authentication failures"; continue; } + + $initial = $matches[1]; + $result = Invoke-SmtpAuthPlain -Reader ($reader) -Writer ($writer) -InitialResponse ($initial); + + if ($result.ProtocolError) { + $authFailures++; + Write-SmtpLine -Writer ($writer) -Line "501 5.5.2 Invalid authentication data"; + } elseif ($result.Valid) { + $authenticated = $true; + $authenticatedUser = $result.Username; + $authFailures = 0; + Write-SmtpLine -Writer ($writer) -Line "235 2.7.0 Authentication successful"; + Write-Log ("SMTP-AUTH PLAIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp, $authenticatedUser); + continue; + } else { + $authFailures++; + Write-SmtpLine -Writer ($writer) -Line "535 5.7.8 Authentication credentials invalid"; + Write-Log ("SMTP-AUTH PLAIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp, $result.Username) "WARN"; } - if ($authenticated) { - Write-SmtpLine $writer "503 5.5.0 Already authenticated" - continue - } - - if ($authLocked) { - Write-SmtpLine $writer "454 4.7.0 Too many authentication failures" - continue - } - - $initial =$matches[1] - $result = Invoke-SmtpAuthPlain -Reader$reader -Writer $writer -InitialResponse$initial - - if ($result.ProtocolError) {$authFailures++ - Write-SmtpLine $writer "501 5.5.2 Invalid authentication data" - } - elseif ($result.Valid) { - $authenticated =$true - $authenticatedUser =$result.Username - $authFailures = 0 - Write-SmtpLine $writer "235 2.7.0 Authentication successful" - Write-Log ("SMTP-AUTH PLAIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp,$authenticatedUser) - continue - } - else { - $authFailures++ - Write-SmtpLine $writer "535 5.7.8 Authentication credentials invalid" - Write-Log ("SMTP-AUTH PLAIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp,$result.Username) "WARN" - } - - if ($authFailures -ge$authSettings.AuthMaxFailures) { - $authLocked =$true - Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures,$remoteIp) "WARN" + if ($authFailures -ge $authSettings.AuthMaxFailures) { + $authLocked = $true; + Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures, $remoteIp) "WARN"; } } elseif ($line -match '^(?i)AUTH\b') { - Write-SmtpLine $writer "504 5.5.4 Unsupported authentication mechanism" + Write-SmtpLine -Writer ($writer) -Line "504 5.5.4 Unsupported authentication mechanism"; } elseif ($line -match '^(?i)MAIL FROM:\s*<([^>]*)>') { - if ($authRequired -and -not$authenticated) { - Write-SmtpLine $writer "530 5.7.0 Authentication required" - continue + if ($authRequired -and -not $authenticated) { Write-SmtpLine -Writer ($writer) -Line "530 5.7.0 Authentication required"; continue; } + if ($acceptedMessages -ge $limits.MaxMessagesPerConnection) { + Write-SmtpLine -Writer ($writer) -Line "452 4.5.3 Too many messages in this session"; + Write-Log ("SMTP-Session von {0}: Nachrichtenlimit {1} erreicht." -f $remoteIp, $limits.MaxMessagesPerConnection) "WARN"; + continue; } - - if ($acceptedMessages -ge$limits.MaxMessagesPerConnection) { - Write-SmtpLine $writer "452 4.5.3 Too many messages in this session" - Write-Log ("SMTP-Session von {0}: Nachrichtenlimit {1} erreicht." -f $remoteIp,$limits.MaxMessagesPerConnection) "WARN" - continue - } - - $mailFrom = $matches[1]$recipients.Clear() - Write-SmtpLine $writer "250 2.1.0 OK" + $mailFrom = $matches[1]; + $recipients.Clear(); + Write-SmtpLine -Writer ($writer) -Line "250 2.1.0 OK"; } elseif ($line -match '^(?i)RCPT TO:\s*<([^>]+)>') { - if (-not $mailFrom) { - Write-SmtpLine $writer "503 5.5.1 Need MAIL FROM first" - continue + if (-not $mailFrom) { Write-SmtpLine -Writer ($writer) -Line "503 5.5.1 Need MAIL FROM first"; continue; } + if ($recipients.Count -ge $limits.MaxRecipients) { + Write-SmtpLine -Writer ($writer) -Line "452 4.5.3 Too many recipients"; + Write-Log ("SMTP-Session von {0}: Empfaengerlimit {1} erreicht." -f $remoteIp, $limits.MaxRecipients) "WARN"; + continue; } - - if ($recipients.Count -ge$limits.MaxRecipients) { - Write-SmtpLine $writer "452 4.5.3 Too many recipients" - Write-Log ("SMTP-Session von {0}: Empfaengerlimit {1} erreicht." -f $remoteIp,$limits.MaxRecipients) "WARN" - continue - } - - $recipients.Add($matches[1]) - Write-SmtpLine $writer "250 2.1.5 OK" + $recipients.Add($matches[1]); + Write-SmtpLine -Writer ($writer) -Line "250 2.1.5 OK"; } elseif ($line -match '^(?i)DATA\s*$') { - if ($authRequired -and -not$authenticated) { - Write-SmtpLine $writer "530 5.7.0 Authentication required" - continue - } + if ($authRequired -and -not $authenticated) { Write-SmtpLine -Writer ($writer) -Line "530 5.7.0 Authentication required"; continue; } + if (-not $mailFrom -or $recipients.Count -eq 0) { Write-SmtpLine -Writer ($writer) -Line "503 5.5.1 Need MAIL FROM and RCPT TO first"; continue; } + if ($acceptedMessages -ge $limits.MaxMessagesPerConnection) { Write-SmtpLine -Writer ($writer) -Line "452 4.5.3 Too many messages in this session"; continue; } - if (-not $mailFrom -or$recipients.Count -eq 0) { - Write-SmtpLine $writer "503 5.5.1 Need MAIL FROM and RCPT TO first" - continue - } - - if ($acceptedMessages -ge$limits.MaxMessagesPerConnection) { - Write-SmtpLine $writer "452 4.5.3 Too many messages in this session" - continue - } - - $pressure = Get-QueuePressure + $pressure = Get-QueuePressure; if (-not $pressure.Accept) { - Write-SmtpLine $writer$pressure.SmtpCode - Write-Log ("SMTP-Backpressure fuer {0}: {1}" -f $remoteIp,$pressure.Reason) "WARN" - continue + Write-SmtpLine -Writer ($writer) -Line ($pressure.SmtpCode); + Write-Log ("SMTP-Backpressure fuer {0}: {1}" -f $remoteIp, $pressure.Reason) "WARN"; + continue; } - Write-SmtpLine $writer "354 End data with ." + Write-SmtpLine -Writer ($writer) -Line "354 End data with ."; - $data = New-Object System.Collections.Generic.List[string]$size = 0 - $maxBytes = [int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024 - $tooLarge =$false + $data = New-Object System.Collections.Generic.List[string]; + $size = 0; + $maxBytes = [int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024; + $tooLarge = $false; while ($true) { - $dataLine =$reader.ReadLine() + $dataLine = $reader.ReadLine(); + if ($null -eq $dataLine) { throw "Client disconnected during DATA"; } + if ($dataLine -eq ".") { break; } + if ($dataLine.StartsWith("..")) { $dataLine = $dataLine.Substring(1); } - if ($null -eq$dataLine) { - throw "Client disconnected during DATA" - } + $size += $latin1.GetByteCount($dataLine) + 2; - if ($dataLine -eq ".") { - break - } - - if ($dataLine.StartsWith("..")) { - $dataLine =$dataLine.Substring(1) - } - - $size += $latin1.GetByteCount($dataLine) + 2 - - if ($size -gt$maxBytes) { - $tooLarge =$true - } - elseif (-not $tooLarge) { - $data.Add($dataLine) - } + if ($size -gt $maxBytes) { $tooLarge = $true; } elseif (-not $tooLarge) { $data.Add($dataLine); } } if ($tooLarge) { - Write-SmtpLine $writer "552 5.3.4 Message size exceeds fixed maximum message size" - Write-Log "Mail von $remoteIp wegen Groessenlimit verworfen." "WARN" - } - else { - $queueId = New-QueueId - + Write-SmtpLine -Writer ($writer) -Line "552 5.3.4 Message size exceeds fixed maximum message size"; + Write-Log "Mail von $remoteIp wegen Groessenlimit verworfen." "WARN"; + } else { + $queueId = New-QueueId; try { - $saved = Save-SmtpMessage ` - -Lines $data ` - -MailFrom $mailFrom ` - -Recipients $recipients.ToArray() ` - -RemoteAddress $remoteIp.ToString() ` - -AuthenticatedUser $authenticatedUser ` - -QueueId $queueId - - $acceptedMessages++ - - Write-SmtpLine $writer ( - "250 2.0.0 Message accepted for delivery; queue-id={0}" -f $saved.QueueId - ) - } - catch { - Write-SmtpLine $writer "451 4.3.0 SMTPGraphRelay queue temporarily unavailable" - - Write-Log ( - "[{0}] Queue-Speicherung fehlgeschlagen | Client={1} | Fehler={2}" -f ` - $queueId, - $remoteIp, - $_.Exception.Message - ) "ERROR" + $rcptArr = $recipients.ToArray(); + $rmtIpStr = $remoteIp.ToString(); + $saved = Save-SmtpMessage -Lines ($data) -MailFrom ($mailFrom) -Recipients ($rcptArr) -RemoteAddress ($rmtIpStr) -AuthenticatedUser ($authenticatedUser) -QueueId ($queueId); + $acceptedMessages++; + Write-SmtpLine -Writer ($writer) -Line ("250 2.0.0 Message accepted for delivery; queue-id={0}" -f $saved.QueueId); + } catch { + Write-SmtpLine -Writer ($writer) -Line "451 4.3.0 SMTPGraphRelay queue temporarily unavailable"; + Write-Log ("[{0}] Queue-Speicherung fehlgeschlagen | Client={1} | Fehler={2}" -f $queueId, $remoteIp, $_.Exception.Message) "ERROR"; } } - - $mailFrom = $null - $recipients.Clear() + $mailFrom = $null; + $recipients.Clear(); } elseif ($line -match '^(?i)RSET\s*$') { - $mailFrom = $null - $recipients.Clear() - Write-SmtpLine $writer "250 2.0.0 Reset" - } - elseif ($line -match '^(?i)NOOP(?:\s+.*)?$') { - Write-SmtpLine $writer "250 2.0.0 OK" - } - elseif ($line -match '^(?i)QUIT\s*$') { - Write-SmtpLine $writer "221 2.0.0 Bye" - break - } - elseif ($line -match '^(?i)STARTTLS\b') { - Write-SmtpLine $writer "502 5.5.1 Command not implemented" - } - else { - Write-SmtpLine $writer "500 5.5.2 Command unrecognized" + $mailFrom = $null; + $recipients.Clear(); + Write-SmtpLine -Writer ($writer) -Line "250 2.0.0 Reset"; } + elseif ($line -match '^(?i)NOOP(?:\s+.*)?$') { Write-SmtpLine -Writer ($writer) -Line "250 2.0.0 OK"; } + elseif ($line -match '^(?i)QUIT\s*$') { Write-SmtpLine -Writer ($writer) -Line "221 2.0.0 Bye"; break; } + elseif ($line -match '^(?i)STARTTLS\b') { Write-SmtpLine -Writer ($writer) -Line "502 5.5.1 Command not implemented"; } + else { Write-SmtpLine -Writer ($writer) -Line "500 5.5.2 Command unrecognized"; } } } - catch { - Write-Log "SMTP-Clientfehler ${remoteIp}: $($_.Exception.Message)" "WARN" - } + catch { Write-Log "SMTP-Clientfehler ${remoteIp}: $($_.Exception.Message)" "WARN"; } finally { - try { $reader.Dispose() } catch {} - try { $writer.Dispose() } catch {} - try { $stream.Dispose() } catch {} - try { $Client.Close() } catch {} + try { $reader.Dispose(); } catch {} + try { $writer.Dispose(); } catch {} + try { $stream.Dispose(); } catch {} + try { $Client.Close(); } catch {} } } function Get-RelayCertificateStatus { - $thumbprint = [string]$script:Config.Graph.CertificateThumbprint + $thumbprint = [string]$script:Config.Graph.CertificateThumbprint; + if ([string]::IsNullOrWhiteSpace($thumbprint)) { throw "Graph.CertificateThumbprint fehlt in config.json."; } - if ([string]::IsNullOrWhiteSpace($thumbprint)) { - throw "Graph.CertificateThumbprint fehlt in config.json." - } + $certPath = "Cert:\LocalMachine\My\$thumbprint"; + $cert = Get-Item -LiteralPath ($certPath) -ErrorAction Stop; - $certPath = "Cert:\LocalMachine\My\$thumbprint" - $cert = Get-Item -LiteralPath $certPath -ErrorAction Stop + if (-not $cert.HasPrivateKey) { throw "Relay-Zertifikat '$thumbprint' besitzt keinen privaten Schluessel."; } - if (-not $cert.HasPrivateKey) { - throw "Relay-Zertifikat '$thumbprint' besitzt keinen privaten Schluessel." - } - - $remaining = $cert.NotAfter.ToUniversalTime() - [DateTime]::UtcNow + $remaining = $cert.NotAfter.ToUniversalTime() - [DateTime]::UtcNow; return [pscustomobject]@{ - Certificate = $cert - Thumbprint = $cert.Thumbprint - Subject = $cert.Subject - NotBefore = $cert.NotBefore - NotAfter = $cert.NotAfter - DaysRemaining = [Math]::Floor($remaining.TotalDays) - HoursRemaining = [Math]::Floor($remaining.TotalHours) - Expired = ($remaining.TotalSeconds -le 0) - } + Certificate = $cert; + Thumbprint = $cert.Thumbprint; + Subject = $cert.Subject; + NotBefore = $cert.NotBefore; + NotAfter = $cert.NotAfter; + DaysRemaining = [Math]::Floor($remaining.TotalDays); + HoursRemaining = [Math]::Floor($remaining.TotalHours); + Expired = ($remaining.TotalSeconds -le 0); + }; } function Test-RelayCertificateExpiry { - param( - [switch]$ForceLog - ) + param([switch]$ForceLog); try { - $status = Get-RelayCertificateStatus + $status = Get-RelayCertificateStatus; + $warningDays = 60; + $criticalDays = 14; - $warningDays = 60 - $criticalDays = 14 - - if ($script:Config.Graph.PSObject.Properties.Name -contains "CertificateWarningDays") { - try { $warningDays = [int]$script:Config.Graph.CertificateWarningDays } catch {} - } - - if ($script:Config.Graph.PSObject.Properties.Name -contains "CertificateCriticalDays") { - try { $criticalDays = [int]$script:Config.Graph.CertificateCriticalDays } catch {} - } + if ($script:Config.Graph.PSObject.Properties.Name -contains "CertificateWarningDays") { try { $warningDays = [int]$script:Config.Graph.CertificateWarningDays; } catch {} } + if ($script:Config.Graph.PSObject.Properties.Name -contains "CertificateCriticalDays") { try { $criticalDays = [int]$script:Config.Graph.CertificateCriticalDays; } catch {} } if ($status.Expired) { - Write-Log ("KRITISCH: Graph-Zertifikat {0} ist seit {1} abgelaufen!" -f ` - $status.Thumbprint, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "ERROR" - return $status + Write-Log ("KRITISCH: Graph-Zertifikat {0} ist seit {1} abgelaufen!" -f $status.Thumbprint, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "ERROR"; + return $status; } - if ($status.DaysRemaining -le$criticalDays) { - Write-Log ("KRITISCH: Graph-Zertifikat laeuft in {0} Tagen ab ({1}). Bitte Zertifikat erneuern." -f ` - $status.DaysRemaining, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "ERROR" - } - elseif ($status.DaysRemaining -le $warningDays) { - Write-Log ("WARNUNG: Graph-Zertifikat laeuft in {0} Tagen ab ({1}). Zertifikatsrotation einplanen." -f ` - $status.DaysRemaining, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "WARN" - } - elseif ($ForceLog) { - Write-Log ("Graph-Zertifikat gueltig bis {0} ({1} Tage verbleibend)." -f ` - $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"), $status.DaysRemaining) + if ($status.DaysRemaining -le $criticalDays) { + Write-Log ("KRITISCH: Graph-Zertifikat laeuft in {0} Tagen ab ({1}). Bitte Zertifikat erneuern." -f $status.DaysRemaining, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "ERROR"; + } elseif ($status.DaysRemaining -le $warningDays) { + Write-Log ("WARNUNG: Graph-Zertifikat laeuft in {0} Tagen ab ({1}). Zertifikatsrotation einplanen." -f $status.DaysRemaining, $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss")) "WARN"; + } elseif ($ForceLog) { + Write-Log ("Graph-Zertifikat gueltig bis {0} ({1} Tage verbleibend)." -f $status.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"), $status.DaysRemaining); } - return $status - } - catch { - Write-Log ("Zertifikatspruefung fehlgeschlagen: {0}" -f $_.Exception.Message) "ERROR" - return $null + return $status; + } catch { + Write-Log ("Zertifikatspruefung fehlgeschlagen: {0}" -f $_.Exception.Message) "ERROR"; + return $null; } } function Get-FunctionBootstrap { - param( - [Parameter(Mandatory)] - [string[]]$FunctionNames - ) - - $parts = New-Object System.Collections.Generic.List[string] + param([Parameter(Mandatory)][string[]]$FunctionNames); + $parts = New-Object System.Collections.Generic.List[string]; foreach ($name in $FunctionNames) { - $item = Get-Item -LiteralPath ("Function:\{0}" -f $name) -ErrorAction Stop - $parts.Add(("function {0} {{`r`n{1}`r`n}}" -f $name, $item.Definition)) + $item = Get-Item -LiteralPath ("Function:\{0}" -f $name) -ErrorAction Stop; + $parts.Add(("function {0} {{`r`n{1}`r`n}}" -f $name, $item.Definition)); } - - return ($parts -join "`r`n`r`n") + return ($parts -join "`r`n`r`n"); } function New-WorkerConfig { - # Runspaces haben keinen verlaesslichen $PSScriptRoot des Hauptskripts. - # Deshalb werden alle Pfade fuer Worker einmal absolut aufgeloest. - $copy = $script:Config | ConvertTo-Json -Depth 20 | ConvertFrom-Json - - $copy.Paths.Queue = Resolve-PathFromConfig $script:Config.Paths.Queue - $copy.Paths.Failed = Resolve-PathFromConfig $script:Config.Paths.Failed - $copy.Paths.Logs = Resolve-PathFromConfig $script:Config.Paths.Logs + $copy = $script:Config | ConvertTo-Json -Depth 20 | ConvertFrom-Json; + $copy.Paths.Queue = Resolve-PathFromConfig ($script:Config.Paths.Queue); + $copy.Paths.Failed = Resolve-PathFromConfig ($script:Config.Paths.Failed); + $copy.Paths.Logs = Resolve-PathFromConfig ($script:Config.Paths.Logs); if ($script:ShutdownSignalPath) { - $copy | Add-Member -NotePropertyName ShutdownSignalPath -NotePropertyValue $script:ShutdownSignalPath -Force + $copy | Add-Member -NotePropertyName ShutdownSignalPath -NotePropertyValue ($script:ShutdownSignalPath) -Force; } - - return $copy + return $copy; } function Send-ServiceBusyAndClose { - param( - [Parameter(Mandatory)] - [System.Net.Sockets.TcpClient]$Client - ) - + param([Parameter(Mandatory)][System.Net.Sockets.TcpClient]$Client); try { - $stream = $Client.GetStream(); $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII, 1024, $true) - $writer.NewLine = "`r`n" - $writer.AutoFlush = $true - $writer.WriteLine("421 4.3.2 SMTPGraphRelay busy, try again later") - $writer.Flush() - $writer.Dispose() - $stream.Dispose() - } - catch {} - finally { - try { $Client.Close() } catch {} - } + $stream = $Client.GetStream(); + $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII, 1024, $true); + $writer.NewLine = "`r`n"; + $writer.AutoFlush = $true; + $writer.WriteLine("421 4.3.2 SMTPGraphRelay busy, try again later"); + $writer.Flush(); + $writer.Dispose(); + $stream.Dispose(); + } catch {} finally { try { $Client.Close(); } catch {} } } function Remove-CompletedSmtpWorkers { for ($i = $script:ActiveSmtpWorkers.Count - 1; $i -ge 0; $i--) { - $worker = $script:ActiveSmtpWorkers[$i] - + $worker = $script:ActiveSmtpWorkers[$i]; if ($worker.AsyncResult.IsCompleted) { - try { - [void]$worker.PowerShell.EndInvoke($worker.AsyncResult) - } - catch { - Write-Log ("SMTP-Worker fuer {0} wurde mit Fehler beendet: {1}" -f $worker.RemoteAddress, $_.Exception.Message) "WARN" - } - finally { - try { $worker.PowerShell.Dispose() } catch {} - $script:ActiveSmtpWorkers.RemoveAt($i) - } + try { [void]$worker.PowerShell.EndInvoke($worker.AsyncResult); } + catch { Write-Log ("SMTP-Worker fuer {0} wurde mit Fehler beendet: {1}" -f $worker.RemoteAddress, $_.Exception.Message) "WARN"; } + finally { try { $worker.PowerShell.Dispose(); } catch {} $script:ActiveSmtpWorkers.RemoveAt($i); } } } } function Start-SmtpClientWorker { - param( - [Parameter(Mandatory)] - [System.Net.Sockets.TcpClient]$Client - ) + param([Parameter(Mandatory)][System.Net.Sockets.TcpClient]$Client); + Remove-CompletedSmtpWorkers; - Remove-CompletedSmtpWorkers - - $remoteAddress = "unknown" - try { - $remoteAddress = ([System.Net.IPEndPoint]$Client.Client.RemoteEndPoint).Address.ToString() - } catch {} + $remoteAddress = "unknown"; + try { $remoteAddress = ([System.Net.IPEndPoint]$Client.Client.RemoteEndPoint).Address.ToString(); } catch {} if ($script:ActiveSmtpWorkers.Count -ge $script:MaxConcurrentClients) { - Write-Log ("SMTP-Verbindung von {0} abgewiesen: Parallel-Limit {1} erreicht." -f $remoteAddress, $script:MaxConcurrentClients) "WARN" - Send-ServiceBusyAndClose -Client $Client - return + Write-Log ("SMTP-Verbindung von {0} abgewiesen: Parallel-Limit {1} erreicht." -f $remoteAddress, $script:MaxConcurrentClients) "WARN"; + Send-ServiceBusyAndClose -Client ($Client); + return; } - $ps = [System.Management.Automation.PowerShell]::Create() - $ps.RunspacePool = $script:SmtpRunspacePool + $ps = [System.Management.Automation.PowerShell]::Create(); + $ps.RunspacePool = $script:SmtpRunspacePool; - [void]$ps.AddScript($script:SmtpWorkerScript) - [void]$ps.AddArgument($Client) - [void]$ps.AddArgument($script:WorkerConfig) + [void]$ps.AddScript($script:SmtpWorkerScript); + [void]$ps.AddArgument($Client); + [void]$ps.AddArgument($script:WorkerConfig); try { - $async = $ps.BeginInvoke() - - [void]$script:ActiveSmtpWorkers.Add([pscustomobject]@{ - PowerShell = $ps - AsyncResult = $async - Client = $Client - RemoteAddress = $remoteAddress - StartedUtc = [DateTime]::UtcNow - }) - } - catch { - try { $ps.Dispose() } catch {} - try { $Client.Close() } catch {} - throw + $async = $ps.BeginInvoke(); + [void]$script:ActiveSmtpWorkers.Add([pscustomobject]@{ PowerShell = $ps; AsyncResult = $async; Client = $Client; RemoteAddress = $remoteAddress; StartedUtc = [DateTime]::UtcNow; }); + } catch { + try { $ps.Dispose(); } catch {} + try { $Client.Close(); } catch {} + throw; } } -if (-not (Test-Path -LiteralPath $ConfigPath)) { - throw "Konfiguration nicht gefunden: $ConfigPath. Bitte zuerst Setup-SMTPGraphRelay.ps1 ausfuehren." +if (-not (Test-Path -LiteralPath ($ConfigPath))) { + throw "Konfiguration nicht gefunden: $ConfigPath. Bitte zuerst Setup-SMTPGraphRelay.ps1 ausfuehren."; } -$script:Config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json - -$script:InstallRoot = Split-Path -Parent ([IO.Path]::GetFullPath($ConfigPath)) -$script:ShutdownSignalPath = Join-Path $script:InstallRoot "shutdown.request" -$script:GracefulShutdownSeconds = 30 +$script:Config = Get-Content -LiteralPath ($ConfigPath) -Raw -Encoding UTF8 | ConvertFrom-Json; +$fullConfPath = [IO.Path]::GetFullPath($ConfigPath); +$script:InstallRoot = Split-Path -Parent ($fullConfPath); +$script:ShutdownSignalPath = Join-Path ($script:InstallRoot) "shutdown.request"; +$script:GracefulShutdownSeconds = 30; if ($script:Config.Smtp.PSObject.Properties.Name -contains "GracefulShutdownSeconds") { try { - $configuredGrace = [int]$script:Config.Smtp.GracefulShutdownSeconds - if ($configuredGrace -ge 5 -and $configuredGrace -le 300) { - $script:GracefulShutdownSeconds = $configuredGrace - } + $configuredGrace = [int]$script:Config.Smtp.GracefulShutdownSeconds; + if ($configuredGrace -ge 5 -and $configuredGrace -le 300) { $script:GracefulShutdownSeconds = $configuredGrace; } } catch {} } -# Ein altes Signal darf einen frisch gestarteten Relay nicht sofort wieder beenden. -Remove-Item -LiteralPath $script:ShutdownSignalPath -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath ($script:ShutdownSignalPath) -Force -ErrorAction SilentlyContinue; +$logResPath = Resolve-PathFromConfig ($script:Config.Paths.Logs); +New-Item -ItemType Directory -Path ($logResPath) -Force | Out-Null; +Initialize-QueueDirectories; +Invoke-LogMaintenance -Force; +[void](Test-RelayCertificateExpiry -ForceLog); -New-Item -ItemType Directory -Path (Resolve-PathFromConfig $script:Config.Paths.Logs) -Force | Out-Null -Initialize-QueueDirectories - -# Alte Logarchive beim Start direkt bereinigen. -Invoke-LogMaintenance -Force - -# Zertifikat beim Start immer pruefen und Status protokollieren. -[void](Test-RelayCertificateExpiry -ForceLog) - -# Pruefintervall optional per config, Standard 12 Stunden. -$script:CertificateCheckHours = 12 +$script:CertificateCheckHours = 12; if ($script:Config.Graph.PSObject.Properties.Name -contains "CertificateCheckHours") { try { - $configuredHours = [int]$script:Config.Graph.CertificateCheckHours - if ($configuredHours -ge 1 -and $configuredHours -le 168) { - $script:CertificateCheckHours = $configuredHours - } + $configuredHours = [int]$script:Config.Graph.CertificateCheckHours; + if ($configuredHours -ge 1 -and $configuredHours -le 168) { $script:CertificateCheckHours = $configuredHours; } } catch {} } -$script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours) +$script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours); -# MaxConcurrentClients ist optional, damit bestehende config.json-Dateien unveraendert weiterlaufen. -$script:MaxConcurrentClients = 20 +$script:MaxConcurrentClients = 20; if ($script:Config.Smtp.PSObject.Properties.Name -contains "MaxConcurrentClients") { try { - $configuredMax = [int]$script:Config.Smtp.MaxConcurrentClients - if ($configuredMax -ge 1 -and $configuredMax -le 200) { - $script:MaxConcurrentClients = $configuredMax - } - else { - Write-Log "Ungueltiges Smtp.MaxConcurrentClients; verwende Standard 20." "WARN" - } - } - catch { - Write-Log "Smtp.MaxConcurrentClients konnte nicht gelesen werden; verwende Standard 20." "WARN" - } + $configuredMax = [int]$script:Config.Smtp.MaxConcurrentClients; + if ($configuredMax -ge 1 -and $configuredMax -le 200) { $script:MaxConcurrentClients = $configuredMax; } else { Write-Log "Ungueltiges Smtp.MaxConcurrentClients; verwende Standard 20." "WARN"; } + } catch { Write-Log "Smtp.MaxConcurrentClients konnte nicht gelesen werden; verwende Standard 20." "WARN"; } } -$script:WorkerConfig = New-WorkerConfig +$script:WorkerConfig = New-WorkerConfig; -# --------------------------------------------------------------------------- -# SMTP Runspace Pool -# --------------------------------------------------------------------------- -# Nur die Funktionen, die ein SMTP-Client wirklich benoetigt, werden in die -# Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker. -$smtpFunctionNames = @( - "Get-LogSettings", - "Invoke-LogMaintenance", - "Rotate-LogIfNeeded", - "Write-Log", - "Resolve-PathFromConfig", - "Test-IPv4InCidr", - "Test-ClientAllowed", - "Get-QueueDirectories", - "New-QueueId", - "Add-RelayMessageHeaders", - "Test-AddressInList", - "Get-SmtpAuthSettings", - "Test-SmtpAuthenticationRequired", - "ConvertFrom-Base64Utf8", - "Invoke-Pbkdf2Sha256", - "Test-FixedTimeEquals", - "Test-SmtpCredentials", - "Invoke-SmtpAuthLogin", - "Invoke-SmtpAuthPlain", - "Get-DebugSettings", - "Get-SmtpLimits", - "Get-QueuePressure", - "Save-SmtpMessage", - "Write-SmtpLine", - "Handle-SmtpClient" -) - -$smtpBootstrap = Get-FunctionBootstrap -FunctionNames $smtpFunctionNames +$smtpFunctionNames = @("Get-LogSettings", "Invoke-LogMaintenance", "Rotate-LogIfNeeded", "Write-Log", "Resolve-PathFromConfig", "Test-IPv4InCidr", "Test-ClientAllowed", "Get-QueueDirectories", "New-QueueId", "Add-RelayMessageHeaders", "Test-AddressInList", "Get-SmtpAuthSettings", "Test-SmtpAuthenticationRequired", "ConvertFrom-Base64Utf8", "Invoke-Pbkdf2Sha256", "Test-FixedTimeEquals", "Test-SmtpCredentials", "Invoke-SmtpAuthLogin", "Invoke-SmtpAuthPlain", "Get-DebugSettings", "Get-SmtpLimits", "Get-QueuePressure", "Save-SmtpMessage", "Write-SmtpLine", "Handle-SmtpClient"); +$smtpBootstrap = Get-FunctionBootstrap -FunctionNames ($smtpFunctionNames); $script:SmtpWorkerScript = @" -param(`$Client, `$Config) -`$script:Config = `$Config - +param(`$Client, `$Config); +`$script:Config = `$Config; $smtpBootstrap +Handle-SmtpClient -Client (`$Client); +"@; -Handle-SmtpClient -Client `$Client -"@ - -$script:SmtpRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool( 1,$script:MaxConcurrentClients -) -$script:SmtpRunspacePool.Open(); $script:ActiveSmtpWorkers = New-Object System.Collections.ArrayList - -# --------------------------------------------------------------------------- -# Separater Queue-/Graph-Worker -# --------------------------------------------------------------------------- -# Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen -# blockiert, laeuft Process-Queue dauerhaft in einem eigenen Runspace. -$queueFunctionNames = @( - "Get-LogSettings", - "Invoke-LogMaintenance", - "Rotate-LogIfNeeded", - "Write-Log", - "Resolve-PathFromConfig", - "Get-GraphConnection", - "Set-MimeSender", - "Get-QueueDirectories", - "Get-GraphFailureInfo", - "Get-RetryDecision", - "Get-DebugSettings", - "Move-QueueItem", - "Send-QueuedMail", - "Process-Queue" -) - -$queueBootstrap = Get-FunctionBootstrap -FunctionNames $queueFunctionNames$queueWorkerScript = @" -param(`$Config) -`$script:Config = `$Config +$script:SmtpRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1,$script:MaxConcurrentClients); +$script:SmtpRunspacePool.Open();$script:ActiveSmtpWorkers = New-Object System.Collections.ArrayList; +$queueFunctionNames = @("Get-LogSettings", "Invoke-LogMaintenance", "Rotate-LogIfNeeded", "Write-Log", "Resolve-PathFromConfig", "Get-GraphConnection", "Set-MimeSender", "Get-QueueDirectories", "Get-GraphFailureInfo", "Get-RetryDecision", "Get-DebugSettings", "Move-QueueItem", "Send-QueuedMail", "Process-Queue"); +$queueBootstrap = Get-FunctionBootstrap -FunctionNames ($queueFunctionNames);$queueWorkerScript = @" +param(`$Config); +`$script:Config = `$Config; $queueBootstrap - try { while (`$true) { - if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath" -and - (Test-Path -LiteralPath ([string]`$script:Config.ShutdownSignalPath))) { - Write-Log "Queue-Worker: Shutdown-Signal erkannt." - break + if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath") { + `$sigPathStr = [string]`$script:Config.ShutdownSignalPath; + if (Test-Path -LiteralPath (`$sigPathStr)) { Write-Log "Queue-Worker: Shutdown-Signal erkannt."; break; } } - - try { - Process-Queue - } - catch { - Write-Log ("Queue-Worker: {0}" -f `$_.Exception.Message) "ERROR" - } - - # In kleinen Schritten schlafen, damit Shutdown nicht erst nach PollSeconds reagiert. - `$sleepUntil = (Get-Date).AddSeconds([int]`$script:Config.Queue.PollSeconds) + try { Process-Queue; } catch { Write-Log ("Queue-Worker: {0}" -f `$_.Exception.Message) "ERROR"; } + `$sleepUntil = (Get-Date).AddSeconds([int]`$script:Config.Queue.PollSeconds); while ((Get-Date) -lt `$sleepUntil) { - if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath" -and - (Test-Path -LiteralPath ([string]`$script:Config.ShutdownSignalPath))) { - break + if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath") { + `$sigPathStr = [string]`$script:Config.ShutdownSignalPath; + if (Test-Path -LiteralPath (`$sigPathStr)) { break; } } - Start-Sleep -Milliseconds 250 + Start-Sleep -Milliseconds 250; } } -} -finally { - try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {} -} -"@ +} finally { try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null; } catch {} } +"@; -$script:QueuePowerShell = [System.Management.Automation.PowerShell]::Create() -[void]$script:QueuePowerShell.AddScript($queueWorkerScript) -[void]$script:QueuePowerShell.AddArgument($script:WorkerConfig) -$script:QueueAsyncResult =$script:QueuePowerShell.BeginInvoke() +$script:QueuePowerShell = [System.Management.Automation.PowerShell]::Create(); +[void]$script:QueuePowerShell.AddScript($queueWorkerScript); +[void]$script:QueuePowerShell.AddArgument($script:WorkerConfig); +$script:QueueAsyncResult =$script:QueuePowerShell.BeginInvoke(); -# --------------------------------------------------------------------------- -# Listener -# --------------------------------------------------------------------------- -$listenIp = [System.Net.IPAddress]::Parse($script:Config.Smtp.ListenAddress) -$listener = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port); $listener.Start() +$listenIp = [System.Net.IPAddress]::Parse($script:Config.Smtp.ListenAddress); +$listener = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port);$listener.Start(); -Write-Log "SMTPGraphRelay V1.9 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)" -Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)" -Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients" -Write-Log "Queue-/Graph-Worker laeuft separat vom SMTP-Listener." +Write-Log "SMTPGraphRelay V1.9 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"; +Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)"; +Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"; +Write-Log "Queue-/Graph-Worker laeuft separat vom SMTP-Listener."; -$limits = Get-SmtpLimits -Write-Log ("SMTP-Limits: max. {0} Empfaenger/Mail, {1} Mails/Verbindung." -f ` - $limits.MaxRecipients, $limits.MaxMessagesPerConnection) -Write-Log ("Backpressure: max. {0} Pending-Mails, mindestens {1} MB freier Speicher." -f ` - $limits.MaxPendingMessages, $limits.MinFreeDiskSpaceMB) +$limits = Get-SmtpLimits; +Write-Log ("SMTP-Limits: max. {0} Empfaenger/Mail, {1} Mails/Verbindung." -f $limits.MaxRecipients, $limits.MaxMessagesPerConnection); +Write-Log ("Backpressure: max. {0} Pending-Mails, mindestens {1} MB freier Speicher." -f $limits.MaxPendingMessages, $limits.MinFreeDiskSpaceMB); -$authSettings = Get-SmtpAuthSettings -$authMode = if ($authSettings.RequireAuth) { "erforderlich" } else { "optional/deaktiviert" } -Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f ` - $authMode, @($authSettings.Users).Count, $authSettings.AuthMaxFailures) +$authSettings = Get-SmtpAuthSettings; +$authMode = if ($authSettings.RequireAuth) { "erforderlich"; } else { "optional/deaktiviert"; }; +$userCount = @($authSettings.Users).Count; +Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f $authMode, $userCount,$authSettings.AuthMaxFailures); +Write-Log ("Graceful Shutdown: bis zu {0} Sekunden fuer aktive Sessions/Worker." -f $script:GracefulShutdownSeconds); -Write-Log ("Graceful Shutdown: bis zu {0} Sekunden fuer aktive Sessions/Worker." -f ` - $script:GracefulShutdownSeconds) +$debugSettings = Get-DebugSettings; +if ($debugSettings.Enabled) { Write-Log "ACHTUNG: QA-/Debug-Testmodus ist AKTIV." "WARN"; } -$debugSettings = Get-DebugSettings -if ($debugSettings.Enabled) { - Write-Log "ACHTUNG: QA-/Debug-Testmodus ist AKTIV." "WARN" -} - -$logSettings = Get-LogSettings -Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f ` - $logSettings.MaxFileSizeMB, $logSettings.RetentionDays) +$logSettings = Get-LogSettings; +Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f $logSettings.MaxFileSizeMB, $logSettings.RetentionDays); try { while ($true) { - Remove-CompletedSmtpWorkers + Remove-CompletedSmtpWorkers; - if (Test-Path -LiteralPath $script:ShutdownSignalPath) { - Write-Log "Graceful Shutdown angefordert. SMTP-Listener wird geschlossen." - try { $listener.Stop() } catch {} - break + if (Test-Path -LiteralPath ($script:ShutdownSignalPath)) { + Write-Log "Graceful Shutdown angefordert. SMTP-Listener wird geschlossen."; + try { $listener.Stop(); } catch {} + break; } if ((Get-Date) -ge $script:NextCertificateCheck) { - [void](Test-RelayCertificateExpiry) - $script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours) + [void](Test-RelayCertificateExpiry); + $script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours); } - # Sollte der Queue-Worker unerwartet beendet werden, ist das nur dann ein - # Fehler, wenn gerade KEIN geplanter Shutdown laeuft. if ($script:QueueAsyncResult.IsCompleted) { - if (Test-Path -LiteralPath $script:ShutdownSignalPath) { - break - } - - try { - [void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult) - throw "Queue-Worker wurde unerwartet beendet." - } - catch { - throw "Queue-Worker wurde unerwartet beendet: $($_.Exception.Message)" - } + if (Test-Path -LiteralPath ($script:ShutdownSignalPath)) { break; } + try { [void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult); throw "Queue-Worker wurde unerwartet beendet."; } catch { throw "Queue-Worker wurde unerwartet beendet: $($_.Exception.Message)"; } } if ($listener.Pending()) { - $client = $listener.AcceptTcpClient() - Start-SmtpClientWorker -Client $client - } - else { - Start-Sleep -Milliseconds 100 + $client =$listener.AcceptTcpClient(); + Start-SmtpClientWorker -Client ($client); + } else { + Start-Sleep -Milliseconds 100; } } } finally { - Write-Log "SMTPGraphRelay faehrt kontrolliert herunter..." + Write-Log "SMTPGraphRelay faehrt kontrolliert herunter..."; + try { $listener.Stop(); } catch {} - try { $listener.Stop() } catch {} + $deadline = (Get-Date).AddSeconds($script:GracefulShutdownSeconds); - $deadline = (Get-Date).AddSeconds($script:GracefulShutdownSeconds) - - # Laufende SMTP-Sessions bekommen Zeit, ihre aktuelle Nachricht/Session sauber - # abzuscliessen. Es werden bereits keine neuen Verbindungen mehr angenommen. - while ($script:ActiveSmtpWorkers.Count -gt 0 -and (Get-Date) -lt $deadline) { - Remove-CompletedSmtpWorkers - - if ($script:ActiveSmtpWorkers.Count -gt 0) { - Start-Sleep -Milliseconds 200 - } + while ($script:ActiveSmtpWorkers.Count -gt 0 -and (Get-Date) -lt$deadline) { + Remove-CompletedSmtpWorkers; + if ($script:ActiveSmtpWorkers.Count -gt 0) { Start-Sleep -Milliseconds 200; } } if ($script:ActiveSmtpWorkers.Count -gt 0) { - Write-Log ("Graceful-Shutdown-Timeout: {0} SMTP-Session(s) werden jetzt beendet." -f ` - $script:ActiveSmtpWorkers.Count) "WARN" - + Write-Log ("Graceful-Shutdown-Timeout: {0} SMTP-Session(s) werden jetzt beendet." -f $script:ActiveSmtpWorkers.Count) "WARN"; foreach ($worker in @($script:ActiveSmtpWorkers)) { - try { $worker.Client.Close() } catch {} - try { $worker.PowerShell.Stop() } catch {} - try { - if ($worker.AsyncResult) { - [void]$worker.PowerShell.EndInvoke($worker.AsyncResult) - } - } catch {} - try { $worker.PowerShell.Dispose() } catch {} + try { $worker.Client.Close(); } catch {} + try { $worker.PowerShell.Stop(); } catch {} + try { if ($worker.AsyncResult) { [void]$worker.PowerShell.EndInvoke($worker.AsyncResult); } } catch {} + try { $worker.PowerShell.Dispose(); } catch {} } - - $script:ActiveSmtpWorkers.Clear() + $script:ActiveSmtpWorkers.Clear(); } - try { $script:SmtpRunspacePool.Close() } catch {} - try { $script:SmtpRunspacePool.Dispose() } catch {} + try { $script:SmtpRunspacePool.Close(); } catch {} + try { $script:SmtpRunspacePool.Dispose(); } catch {} - # Queue-Worker darf den aktuell laufenden Graph-Aufruf beenden. Durch das - # Shutdown-Signal startet Process-Queue anschliessend keine weitere Mail. - while (-not $script:QueueAsyncResult.IsCompleted -and (Get-Date) -lt$deadline) { - Start-Sleep -Milliseconds 200 - } + while (-not $script:QueueAsyncResult.IsCompleted -and (Get-Date) -lt$deadline) { Start-Sleep -Milliseconds 200; } if (-not $script:QueueAsyncResult.IsCompleted) { - Write-Log "Graceful-Shutdown-Timeout: Queue-/Graph-Worker wird jetzt beendet." "WARN" - try { $script:QueuePowerShell.Stop() } catch {} + Write-Log "Graceful-Shutdown-Timeout: Queue-/Graph-Worker wird jetzt beendet." "WARN"; + try { $script:QueuePowerShell.Stop(); } catch {} } - try { - if ($script:QueueAsyncResult) { - [void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult) - } - } catch {} - - try { $script:QueuePowerShell.Dispose() } catch {} - try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {} - - Remove-Item -LiteralPath $script:ShutdownSignalPath -Force -ErrorAction SilentlyContinue - Write-Log "SMTPGraphRelay sauber beendet." -} - - + try { if ($script:QueueAsyncResult) { [void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult); } } catch {} + try { $script:QueuePowerShell.Dispose(); } catch {} + try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null; } catch {} + Remove-Item -LiteralPath ($script:ShutdownSignalPath) -Force -ErrorAction SilentlyContinue; + Write-Log "SMTPGraphRelay sauber beendet."; +} \ No newline at end of file