Dateien nach "/" hochladen
This commit is contained in:
+94
-2
@@ -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)
|
||||
|
||||
@@ -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." }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user