Dateien nach "/" hochladen

This commit is contained in:
2026-08-14 22:58:17 +02:00
parent 9d49321abc
commit f70ee9e763
3 changed files with 801 additions and 6 deletions
+94 -2
View File
@@ -7,7 +7,7 @@
Nimmt lokale SMTP-Mails an, speichert sie als .eml in einer Queue und sendet sie
anschließend per Microsoft Graph sendMail mit App-only Zertifikatsauthentifizierung.
V1.8: Failed-Queue-Verwaltung und kooperativer Graceful Shutdown; inklusive SMTP AUTH und V1.6 Queue-/Backpressure-Funktionen
V1.9: gezielte QA-/Failure-Mode-Testhooks; inklusive Failed-Queue, Graceful Shutdown, SMTP AUTH und Backpressure
#>
[CmdletBinding()]
@@ -580,6 +580,20 @@ function Send-QueuedMail {
}
try {
$debug = Get-DebugSettings
if ($debug.Enabled -and $debug.DelayBeforeGraphSendSeconds -gt 0) {
Write-Log ("[{0}] QA: Graph-Versand wird um {1} Sek. verzögert." -f `
$queueId, $debug.DelayBeforeGraphSendSeconds) "WARN"
Start-Sleep -Seconds ([int]$debug.DelayBeforeGraphSendSeconds)
}
if ($debug.Enabled -and $debug.SimulateGraphStatus -ge 400) {
Write-Log ("[{0}] QA: simulierter Graph HTTP {1}." -f `
$queueId, $debug.SimulateGraphStatus) "WARN"
throw ("QA simulated Graph HTTP {0}" -f $debug.SimulateGraphStatus)
}
Get-GraphConnection
$sender = [Uri]::EscapeDataString($script:Config.Graph.SenderMailbox)
@@ -1128,6 +1142,57 @@ function Invoke-SmtpAuthPlain {
}
}
function Get-DebugSettings {
$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 ($d.PSObject.Properties.Name -contains "DelayBeforeGraphSendSeconds") {
$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 }
}
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 }
}
if ($d.PSObject.Properties.Name -contains "OverrideFreeDiskSpaceMB") {
$v = [int]$d.OverrideFreeDiskSpaceMB
if ($v -ge -1 -and $v -le 1048576) { $defaults.OverrideFreeDiskSpaceMB = $v }
}
}
catch {}
return [pscustomobject]$defaults
}
function Get-SmtpLimits {
$maxRecipients = 50
$maxMessagesPerConnection = 25
@@ -1169,11 +1234,19 @@ function Get-QueuePressure {
$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
@@ -1203,10 +1276,16 @@ function Get-QueuePressure {
if ($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)
}
}
}
}
catch {
return [pscustomobject]@{
Accept = $false
@@ -1290,6 +1369,12 @@ function Save-SmtpMessage {
}
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"
}
# Erst vollständig in incoming schreiben.
[IO.File]::WriteAllText($incomingEmlTmp, $raw, $utf8NoBom)
$meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $incomingMetaTmp -Encoding UTF8
@@ -1918,6 +2003,7 @@ $smtpFunctionNames = @(
"Test-SmtpCredentials",
"Invoke-SmtpAuthLogin",
"Invoke-SmtpAuthPlain",
"Get-DebugSettings",
"Get-SmtpLimits",
"Get-QueuePressure",
"Save-SmtpMessage",
@@ -1958,6 +2044,7 @@ $queueFunctionNames = @(
"Get-QueueDirectories",
"Get-GraphFailureInfo",
"Get-RetryDecision",
"Get-DebugSettings",
"Move-QueueItem",
"Send-QueuedMail",
"Process-Queue"
@@ -2013,7 +2100,7 @@ $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.8 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
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 läuft separat vom SMTP-Listener."
@@ -2032,6 +2119,11 @@ Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f
Write-Log ("Graceful Shutdown: bis zu {0} Sekunden für aktive Sessions/Worker." -f `
$script:GracefulShutdownSeconds)
$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)
+16
View File
@@ -46,6 +46,7 @@ $RemoteRelayUrl = "$RepoBaseUrl/raw/branch/main/SMTPGraphRelay.ps1"
$ManagedReleaseFiles = @(
"SMTPGraphRelay.ps1",
"Test-SMTPGraphRelay.ps1",
"Test-SMTPGraphRelay-FailureModes.ps1",
"Renew-SMTPGraphRelayCertificate.ps1",
"Setup-SMTPGraphRelay.ps1",
"version.json",
@@ -2076,6 +2077,19 @@ function Manage-SmtpAuth {
}
}
function Invoke-FailureModeTests {
Write-Title "SMTPGraphRelay - Failure Mode QA"
$tester = Join-Path $InstallPath "Test-SMTPGraphRelay-FailureModes.ps1"
if (-not (Test-Path -LiteralPath $tester)) {
Write-Fail "Test-SMTPGraphRelay-FailureModes.ps1 ist nicht installiert."
return
}
& $tester -InstallPath $InstallPath
}
function Uninstall-Relay {
Write-Title "SMTPGraphRelay - Deinstallation"
@@ -2241,6 +2255,7 @@ function Show-Menu {
Write-Host " [8] Status anzeigen"
Write-Host " [9] SMTP-AUTH verwalten"
Write-Host " [10] Failed Queue verwalten"
Write-Host " [11] Failure-Mode QA"
Write-Host " [0] Beenden"
Write-Host ""
}
@@ -2263,6 +2278,7 @@ while ($true) {
"8" { Show-Status }
"9" { Manage-SmtpAuth }
"10" { Manage-FailedQueue }
"11" { Invoke-FailureModeTests }
"0" { break }
default { Write-Warn "Ungültige Auswahl." }
}
+687
View File
@@ -0,0 +1,687 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string]$InstallPath = "$env:ProgramFiles\SMTPGraphRelay"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [Text.Encoding]::UTF8
$TaskName = "SMTPGraphRelay"
$ConfigPath = Join-Path $InstallPath "config.json"
$ShutdownSignalPath = Join-Path $InstallPath "shutdown.request"
function Write-Title {
param([string]$Text)
Write-Host ""
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host " $Text" -ForegroundColor Cyan
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host ""
}
function Write-Ok { param([string]$Text) Write-Host "[OK] $Text" -ForegroundColor Green }
function Write-Warn { param([string]$Text) Write-Host "[WARN] $Text" -ForegroundColor Yellow }
function Write-Fail { param([string]$Text) Write-Host "[FAIL] $Text" -ForegroundColor Red }
function Write-Info { param([string]$Text) Write-Host "[INFO] $Text" -ForegroundColor Cyan }
function Get-Config {
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "config.json nicht gefunden: $ConfigPath"
}
return Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
}
function Save-Config {
param([Parameter(Mandatory)]$Config)
$tmp = "$ConfigPath.qa.tmp"
$Config | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $tmp -Encoding UTF8
Move-Item -LiteralPath $tmp -Destination $ConfigPath -Force
}
function Get-FullPath {
param([string]$Value)
if ([IO.Path]::IsPathRooted($Value)) { return $Value }
return Join-Path $InstallPath $Value
}
function Stop-RelayGracefully {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if (-not $task -or $task.State -ne "Running") { return }
[IO.File]::WriteAllText(
$ShutdownSignalPath,
[DateTime]::UtcNow.ToString("o"),
(New-Object Text.UTF8Encoding($false))
)
$deadline = (Get-Date).AddSeconds(40)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 250
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if (-not $task -or $task.State -ne "Running") {
Remove-Item $ShutdownSignalPath -Force -ErrorAction SilentlyContinue
return
}
}
Write-Warn "Graceful Stop dauerte zu lange; Task wird hart gestoppt."
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Remove-Item $ShutdownSignalPath -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
}
function Start-Relay {
Remove-Item $ShutdownSignalPath -Force -ErrorAction SilentlyContinue
Start-ScheduledTask -TaskName $TaskName
$deadline = (Get-Date).AddSeconds(10)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 250
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($task -and $task.State -eq "Running") {
return
}
}
throw "Relay konnte nicht gestartet werden."
}
function Restart-Relay {
Stop-RelayGracefully
Start-Relay
Start-Sleep -Seconds 1
}
function Set-QADebug {
param(
[int]$DelayBeforeGraphSendSeconds = 0,
[int]$SimulateGraphStatus = 0,
[bool]$SimulateQueueWriteFailure = $false,
[int]$OverridePendingCount = -1,
[int]$OverrideFreeDiskSpaceMB = -1
)
$config = Get-Config
$debug = [pscustomobject]@{
Enabled = $true
DelayBeforeGraphSendSeconds = $DelayBeforeGraphSendSeconds
SimulateGraphStatus = $SimulateGraphStatus
SimulateQueueWriteFailure = $SimulateQueueWriteFailure
OverridePendingCount = $OverridePendingCount
OverrideFreeDiskSpaceMB = $OverrideFreeDiskSpaceMB
}
if ($config.PSObject.Properties.Name -contains "Debug") {
$config.Debug = $debug
}
else {
$config | Add-Member -NotePropertyName Debug -NotePropertyValue $debug
}
Save-Config -Config $config
}
function Restore-OriginalConfig {
param([Parameter(Mandatory)][string]$BackupPath)
Copy-Item -LiteralPath $BackupPath -Destination $ConfigPath -Force
}
function Convert-SecureToPlain {
param([Security.SecureString]$Secure)
$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) }
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }
}
function Get-SmtpAuthForLocalTest {
$config = Get-Config
$required = $false
if ($config.Smtp.PSObject.Properties.Name -contains "RequireAuth") {
$required = [bool]$config.Smtp.RequireAuth
}
if (-not $required) {
return [pscustomobject]@{ Username=$null; Password=$null }
}
# Prüfen, ob localhost als Ausnahme eingetragen ist.
foreach ($entry in @($config.Smtp.AllowUnauthenticatedNetworks)) {
if ([string]$entry -in @("127.0.0.1","127.0.0.1/32","127.0.0.0/8","*")) {
return [pscustomobject]@{ Username=$null; Password=$null }
}
}
$user = Read-Host "SMTP-Benutzer für QA-Test"
$secure = Read-Host "SMTP-Passwort" -AsSecureString
$plain = Convert-SecureToPlain -Secure $secure
return [pscustomobject]@{ Username=$user; Password=$plain }
}
function Read-SmtpResponse {
param(
[Parameter(Mandatory)][IO.StreamReader]$Reader
)
$lines = New-Object Collections.Generic.List[string]
$first = $Reader.ReadLine()
if ($null -eq $first) { throw "SMTP-Verbindung unerwartet beendet." }
$lines.Add($first)
if ($first -match '^(\d{3})-') {
$code = $matches[1]
while ($true) {
$line = $Reader.ReadLine()
if ($null -eq $line) { throw "SMTP-Verbindung unerwartet beendet." }
$lines.Add($line)
if ($line -match "^$code ") { break }
}
}
$status = 0
if ($lines[0] -match '^(\d{3})') { $status = [int]$matches[1] }
return [pscustomobject]@{
Code = $status
Text = ($lines -join "`n")
}
}
function Invoke-AuthIfNeeded {
param(
[IO.StreamReader]$Reader,
[IO.StreamWriter]$Writer,
$Auth
)
if ([string]::IsNullOrWhiteSpace([string]$Auth.Username)) { return }
$Writer.WriteLine("AUTH LOGIN")
$r = Read-SmtpResponse -Reader $Reader
if ($r.Code -ne 334) { throw "AUTH LOGIN: $($r.Text)" }
$Writer.WriteLine([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Auth.Username)))
$r = Read-SmtpResponse -Reader $Reader
if ($r.Code -ne 334) { throw "AUTH Benutzer: $($r.Text)" }
$Writer.WriteLine([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Auth.Password)))
$r = Read-SmtpResponse -Reader $Reader
if ($r.Code -ne 235) { throw "AUTH Passwort: $($r.Text)" }
}
function Open-SmtpSession {
$config = Get-Config
$port = [int]$config.Smtp.Port
$client = New-Object Net.Sockets.TcpClient
$client.Connect("127.0.0.1", $port)
$stream = $client.GetStream()
$stream.ReadTimeout = 10000
$reader = New-Object IO.StreamReader($stream, [Text.Encoding]::UTF8, $true, 4096, $true)
$writer = New-Object IO.StreamWriter($stream, [Text.Encoding]::ASCII, 4096, $true)
$writer.NewLine = "`r`n"
$writer.AutoFlush = $true
$greeting = Read-SmtpResponse -Reader $reader
if ($greeting.Code -ne 220) { throw "Greeting: $($greeting.Text)" }
$writer.WriteLine("EHLO qa.local")
$ehlo = Read-SmtpResponse -Reader $reader
if ($ehlo.Code -ne 250) { throw "EHLO: $($ehlo.Text)" }
$auth = Get-SmtpAuthForLocalTest
Invoke-AuthIfNeeded -Reader $reader -Writer $writer -Auth $auth
return [pscustomobject]@{
Client = $client
Stream = $stream
Reader = $reader
Writer = $writer
Config = $config
Auth = $auth
}
}
function Close-SmtpSession {
param($Session)
try { $Session.Writer.WriteLine("QUIT") } catch {}
try { $Session.Reader.Dispose() } catch {}
try { $Session.Writer.Dispose() } catch {}
try { $Session.Stream.Dispose() } catch {}
try { $Session.Client.Close() } catch {}
if ($Session.Auth) { $Session.Auth.Password = $null }
}
function Send-TestMail {
param(
[switch]$StopBeforeBody,
[string]$UniqueTag = $([guid]::NewGuid().ToString("N").Substring(0,10))
)
$s = Open-SmtpSession
try {
$from = [string]$s.Config.Graph.SenderMailbox
$to = Read-Host "Test-Empfänger [Standard: $from]"
if ([string]::IsNullOrWhiteSpace($to)) { $to = $from }
$s.Writer.WriteLine("MAIL FROM:<$from>")
$r = Read-SmtpResponse $s.Reader
if ($r.Code -ne 250) { return [pscustomobject]@{ Session=$s; Response=$r; QueueId=$null; Stage="MAIL" } }
$s.Writer.WriteLine("RCPT TO:<$to>")
$r = Read-SmtpResponse $s.Reader
if ($r.Code -ne 250) { return [pscustomobject]@{ Session=$s; Response=$r; QueueId=$null; Stage="RCPT" } }
$s.Writer.WriteLine("DATA")
$r = Read-SmtpResponse $s.Reader
if ($r.Code -ne 354) {
return [pscustomobject]@{ Session=$s; Response=$r; QueueId=$null; Stage="DATA" }
}
$s.Writer.WriteLine("From: <$from>")
$s.Writer.WriteLine("To: <$to>")
$s.Writer.WriteLine("Subject: SMTPGraphRelay QA $UniqueTag")
$s.Writer.WriteLine("X-SMTPGraphRelay-QA: $UniqueTag")
$s.Writer.WriteLine("")
if ($StopBeforeBody) {
return [pscustomobject]@{ Session=$s; Response=$r; QueueId=$null; Stage="DATA-OPEN"; Tag=$UniqueTag }
}
$s.Writer.WriteLine("QA test $UniqueTag")
$s.Writer.WriteLine(".")
$final = Read-SmtpResponse $s.Reader
$qid = $null
if ($final.Text -match 'queue-id=([A-Fa-f0-9]+)') { $qid = $matches[1] }
return [pscustomobject]@{ Session=$s; Response=$final; QueueId=$qid; Stage="FINAL"; Tag=$UniqueTag }
}
catch {
Close-SmtpSession $s
throw
}
}
function Get-QueuePaths {
$config = Get-Config
$queueRoot = Get-FullPath ([string]$config.Paths.Queue)
return [pscustomobject]@{
Pending = Join-Path $queueRoot "pending"
Processing = Join-Path $queueRoot "processing"
Failed = Get-FullPath ([string]$config.Paths.Failed)
}
}
function Remove-QAMessage {
param([string]$QueueId)
if ([string]::IsNullOrWhiteSpace($QueueId)) { return }
$paths = Get-QueuePaths
foreach ($dir in @($paths.Pending,$paths.Processing,$paths.Failed)) {
foreach ($p in @(
(Join-Path $dir "$QueueId.eml"),
(Join-Path $dir "$QueueId.eml.json")
)) {
Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue
}
}
}
function Wait-ForQueueMeta {
param(
[string]$QueueId,
[int]$TimeoutSeconds = 15
)
$paths = Get-QueuePaths
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
foreach ($dir in @($paths.Pending,$paths.Processing,$paths.Failed)) {
$metaPath = Join-Path $dir "$QueueId.eml.json"
if (Test-Path $metaPath) {
try {
$meta = Get-Content $metaPath -Raw -Encoding UTF8 | ConvertFrom-Json
return [pscustomobject]@{ Directory=$dir; Meta=$meta; Path=$metaPath }
} catch {}
}
}
Start-Sleep -Milliseconds 250
}
return $null
}
function Invoke-WithTemporaryConfig {
param(
[Parameter(Mandatory)][scriptblock]$Configure,
[Parameter(Mandatory)][scriptblock]$Test
)
$backup = Join-Path $env:TEMP ("SMTPGraphRelay-config-qa-{0}.json" -f [guid]::NewGuid().ToString("N"))
Copy-Item $ConfigPath $backup -Force
try {
& $Configure
Restart-Relay
& $Test
}
finally {
try {
Restore-OriginalConfig -BackupPath $backup
Restart-Relay
Write-Ok "Original-config.json wiederhergestellt."
}
catch {
Write-Fail "Config-Restore/Relay-Restart fehlgeschlagen: $($_.Exception.Message)"
}
Remove-Item $backup -Force -ErrorAction SilentlyContinue
}
}
function Test-GraphStatus {
param([int]$StatusCode)
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug -SimulateGraphStatus $StatusCode
} `
-Test {
$result = Send-TestMail
try {
if ($result.Response.Code -ne 250 -or -not $result.QueueId) {
Write-Fail "SMTP-Annahme fehlgeschlagen: $($result.Response.Text)"
return
}
Write-Ok "SMTP hat Testmail angenommen: $($result.QueueId)"
$found = Wait-ForQueueMeta -QueueId $result.QueueId -TimeoutSeconds 15
if (-not $found) {
Write-Fail "Queue-Metadaten nicht gefunden."
return
}
Start-Sleep -Seconds 1
$found = Wait-ForQueueMeta -QueueId $result.QueueId -TimeoutSeconds 5
if ($found -and [int]$found.Meta.RetryCount -ge 1 -and [int]$found.Meta.LastStatusCode -eq $StatusCode) {
Write-Ok "HTTP $StatusCode korrekt als Retry behandelt (RetryCount=$($found.Meta.RetryCount))."
}
else {
Write-Fail "Erwartete Retry-Metadaten für HTTP $StatusCode fehlen."
}
}
finally {
if ($result) {
Close-SmtpSession $result.Session
Remove-QAMessage $result.QueueId
}
}
}
}
function Test-QueueWriteFailure {
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug -SimulateQueueWriteFailure $true
} `
-Test {
$result = Send-TestMail
try {
if ($result.Stage -eq "FINAL" -and $result.Response.Code -eq 451) {
Write-Ok "Queue-Schreibfehler korrekt mit 451 zurückgewiesen."
}
else {
Write-Fail "Erwartet 451 nach DATA, erhalten: $($result.Response.Text)"
}
}
finally {
if ($result) { Close-SmtpSession $result.Session }
}
}
}
function Test-PendingBackpressure {
$config = Get-Config
$limit = if ($config.Queue.PSObject.Properties.Name -contains "MaxPendingMessages") { [int]$config.Queue.MaxPendingMessages } else { 5000 }
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug -OverridePendingCount $limit
} `
-Test {
$result = Send-TestMail
try {
if ($result.Stage -eq "DATA" -and $result.Response.Code -eq 452) {
Write-Ok "Pending-Queue-Limit korrekt vor DATA mit 452 blockiert."
}
else {
Write-Fail "Erwartet 452 bei DATA, erhalten: $($result.Response.Text)"
}
}
finally {
if ($result) { Close-SmtpSession $result.Session }
}
}
}
function Test-DiskBackpressure {
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug -OverrideFreeDiskSpaceMB 0
} `
-Test {
$result = Send-TestMail
try {
if ($result.Stage -eq "DATA" -and $result.Response.Code -eq 452) {
Write-Ok "Disk-Backpressure korrekt vor DATA mit 452 blockiert."
}
else {
Write-Fail "Erwartet 452 bei DATA, erhalten: $($result.Response.Text)"
}
}
finally {
if ($result) { Close-SmtpSession $result.Session }
}
}
}
function Test-GracefulDuringData {
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug
} `
-Test {
$result = Send-TestMail -StopBeforeBody
try {
if ($result.Stage -ne "DATA-OPEN") {
Write-Fail "DATA-Session konnte nicht geöffnet werden."
return
}
Write-Info "DATA ist offen. Fordere jetzt Graceful Shutdown an..."
[IO.File]::WriteAllText($ShutdownSignalPath, [DateTime]::UtcNow.ToString("o"))
Start-Sleep -Seconds 1
$task = Get-ScheduledTask -TaskName $TaskName
if ($task.State -eq "Running") {
Write-Ok "Relay bleibt während aktiver DATA-Session erwartungsgemäß aktiv."
}
else {
Write-Fail "Relay wurde beendet, obwohl DATA-Session noch offen war."
return
}
$result.Session.Writer.WriteLine("QA graceful DATA test")
$result.Session.Writer.WriteLine(".")
$final = Read-SmtpResponse $result.Session.Reader
if ($final.Code -eq 250) {
Write-Ok "Laufende DATA-Mail wurde trotz Shutdown sauber angenommen."
if ($final.Text -match 'queue-id=([A-Fa-f0-9]+)') {
$result.QueueId = $matches[1]
}
}
else {
Write-Fail "Mailabschluss während Shutdown: $($final.Text)"
}
$deadline = (Get-Date).AddSeconds(15)
do {
Start-Sleep -Milliseconds 250
$task = Get-ScheduledTask -TaskName $TaskName
} while ($task.State -eq "Running" -and (Get-Date) -lt $deadline)
if ($task.State -ne "Running") {
Write-Ok "Relay hat sich nach Abschluss der Session sauber beendet."
}
else {
Write-Fail "Relay läuft nach Abschluss der Session weiter."
}
}
finally {
if ($result) {
Close-SmtpSession $result.Session
Remove-QAMessage $result.QueueId
}
Remove-Item $ShutdownSignalPath -Force -ErrorAction SilentlyContinue
}
}
}
function Test-GracefulDuringGraph {
Invoke-WithTemporaryConfig `
-Configure {
Set-QADebug -DelayBeforeGraphSendSeconds 8 -SimulateGraphStatus 500
} `
-Test {
$result = Send-TestMail
try {
if ($result.Response.Code -ne 250 -or -not $result.QueueId) {
Write-Fail "SMTP-Testmail wurde nicht angenommen."
return
}
Close-SmtpSession $result.Session
$result.Session = $null
$paths = Get-QueuePaths
$processing = Join-Path $paths.Processing "$($result.QueueId).eml"
$deadline = (Get-Date).AddSeconds(10)
while (-not (Test-Path $processing) -and (Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 200
}
if (-not (Test-Path $processing)) {
Write-Fail "Mail erreichte processing nicht rechtzeitig."
return
}
Write-Ok "Mail ist in processing / künstlicher Graph-Verzögerung."
[IO.File]::WriteAllText($ShutdownSignalPath, [DateTime]::UtcNow.ToString("o"))
Start-Sleep -Seconds 1
$task = Get-ScheduledTask -TaskName $TaskName
if ($task.State -eq "Running") {
Write-Ok "Relay wartet während aktivem Queue-/Graph-Worker."
}
else {
Write-Fail "Relay wurde zu früh beendet."
}
$deadline = (Get-Date).AddSeconds(20)
do {
Start-Sleep -Milliseconds 250
$task = Get-ScheduledTask -TaskName $TaskName
} while ($task.State -eq "Running" -and (Get-Date) -lt $deadline)
if ($task.State -ne "Running") {
Write-Ok "Relay hat Worker auslaufen lassen und sich danach beendet."
}
else {
Write-Fail "Graceful Shutdown während Queue-/Graph-Worker dauerte zu lange."
}
}
finally {
if ($result -and $result.Session) { Close-SmtpSession $result.Session }
if ($result) { Remove-QAMessage $result.QueueId }
Remove-Item $ShutdownSignalPath -Force -ErrorAction SilentlyContinue
}
}
}
function Clear-QADebug {
$config = Get-Config
if ($config.PSObject.Properties.Name -contains "Debug") {
$config.PSObject.Properties.Remove("Debug")
Save-Config $config
Restart-Relay
Write-Ok "Debug-/QA-Block aus config.json entfernt."
}
else {
Write-Ok "Kein Debug-/QA-Block vorhanden."
}
}
if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) {
throw "Dieses QA-Script muss mit Windows PowerShell 5.1 ausgeführt werden."
}
while ($true) {
Clear-Host
Write-Title "SMTPGraphRelay - Failure Mode QA"
Write-Warn "Dieses Werkzeug erzeugt absichtlich Fehlerzustände."
Write-Host "Die Original-config.json wird für jeden Test gesichert und danach wiederhergestellt."
Write-Host ""
Write-Host " [1] Graph HTTP 429 / Retry testen"
Write-Host " [2] Graph HTTP 500 / Retry testen"
Write-Host " [3] Queue-Schreibfehler / SMTP 451 testen"
Write-Host " [4] Pending-Queue-Backpressure / SMTP 452 testen"
Write-Host " [5] Disk-Backpressure / SMTP 452 testen"
Write-Host " [6] Graceful Shutdown während SMTP DATA testen"
Write-Host " [7] Graceful Shutdown während Queue/Graph testen"
Write-Host " [8] QA-/Debug-Block aus Config entfernen"
Write-Host " [0] Beenden"
Write-Host ""
$choice = Read-Host "Auswahl"
try {
switch ($choice) {
"1" { Test-GraphStatus -StatusCode 429 }
"2" { Test-GraphStatus -StatusCode 500 }
"3" { Test-QueueWriteFailure }
"4" { Test-PendingBackpressure }
"5" { Test-DiskBackpressure }
"6" { Test-GracefulDuringData }
"7" { Test-GracefulDuringGraph }
"8" { Clear-QADebug }
"0" { break }
default { Write-Warn "Ungültige Auswahl." }
}
}
catch {
Write-Fail $_.Exception.Message
}
if ($choice -ne "0") {
Write-Host ""
Read-Host "Enter drücken"
}
if ($choice -eq "0") { break }
}