diff --git a/Setup-SMTPGraphRelay.ps1 b/Setup-SMTPGraphRelay.ps1 index cc6c589..ed8fc73 100644 --- a/Setup-SMTPGraphRelay.ps1 +++ b/Setup-SMTPGraphRelay.ps1 @@ -1459,7 +1459,7 @@ function Get-FailedQueueEntries { MetaPath = $metaPath SizeKB = [Math]::Round($file.Length / 1KB, 1) FailedSince = $file.LastWriteTime - RetryCount = if ($meta -and $meta.RetryCount -ne$null) { [int]$meta.RetryCount } else {$null } + RetryCount = if ($meta -and $meta.RetryCount -ne $null) { [int]$meta.RetryCount } else { $null } From = if ($meta) { [string]$meta.EnvelopeFrom } else { "" } Recipients = if ($meta) { (@($meta.EnvelopeRecipients) -join ", ") } else { "" } LastStatusCode = if ($meta) { [string]$meta.LastStatusCode } else { "" } @@ -1478,10 +1478,10 @@ function Retry-FailedQueueEntry { [Parameter(Mandatory)]$Entry ) - $pendingPath = Get-PendingQueuePath -Config$Config + $pendingPath = Get-PendingQueuePath -Config $Config New-Item -ItemType Directory -Path $pendingPath -Force | Out-Null - $targetEml = Join-Path $pendingPath$Entry.FileName + $targetEml = Join-Path $pendingPath $Entry.FileName $targetMeta = "$targetEml.json" if (Test-Path -LiteralPath $targetEml) { @@ -1489,35 +1489,36 @@ function Retry-FailedQueueEntry { } if ($Entry.Meta) { - $meta =$Entry.Meta + $meta = $Entry.Meta if ($meta.PSObject.Properties.Name -contains "RetryCount") { $meta.RetryCount = 0 - } else {$meta | Add-Member -NotePropertyName RetryCount -NotePropertyValue 0 + } else { + $meta | Add-Member -NotePropertyName RetryCount -NotePropertyValue 0 } $now = [DateTime]::UtcNow.ToString("o") if ($meta.PSObject.Properties.Name -contains "NextAttemptUtc") { - $meta.NextAttemptUtc =$now + $meta.NextAttemptUtc = $now } else { - $meta | Add-Member -NotePropertyName NextAttemptUtc -NotePropertyValue$now + $meta | Add-Member -NotePropertyName NextAttemptUtc -NotePropertyValue $now } if ($meta.PSObject.Properties.Name -contains "RequeuedUtc") { - $meta.RequeuedUtc =$now + $meta.RequeuedUtc = $now } else { - $meta | Add-Member -NotePropertyName RequeuedUtc -NotePropertyValue$now + $meta | Add-Member -NotePropertyName RequeuedUtc -NotePropertyValue $now } - $meta | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath$Entry.MetaPath -Encoding UTF8 + $meta | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $Entry.MetaPath -Encoding UTF8 } if (Test-Path -LiteralPath $Entry.MetaPath) { - Move-Item -LiteralPath $Entry.MetaPath -Destination$targetMeta -Force + Move-Item -LiteralPath $Entry.MetaPath -Destination $targetMeta -Force } - Move-Item -LiteralPath $Entry.Path -Destination$targetEml -Force + Move-Item -LiteralPath $Entry.Path -Destination $targetEml -Force } function Remove-FailedQueueEntry { @@ -1528,7 +1529,7 @@ function Remove-FailedQueueEntry { } function Manage-FailedQueue { - $config = Get-RelayConfig -TargetPath$InstallPath + $config = Get-RelayConfig -TargetPath $InstallPath if (-not $config) { Write-Fail "config.json nicht gefunden." @@ -1539,7 +1540,7 @@ function Manage-FailedQueue { Clear-Host Write-Title "SMTPGraphRelay - Failed Queue" - $entries = @(Get-FailedQueueEntries -Config$config) + $entries = @(Get-FailedQueueEntries -Config $config) Write-Host "Failed-Mails: $($entries.Count)" Write-Host "" @@ -1572,8 +1573,8 @@ function Manage-FailedQueue { "2" { $id = Read-Host "Queue-ID" - $entry =$entries | - Where-Object { $_.QueueId -eq$id -or $_.FileName -eq$id -or $_.FileName -eq "$id.eml" } | + $entry = $entries | + Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } | Select-Object -First 1 if (-not $entry) { @@ -1601,15 +1602,15 @@ function Manage-FailedQueue { "3" { $id = Read-Host "Queue-ID" - $entry =$entries | - Where-Object { $_.QueueId -eq$id -or $_.FileName -eq$id -or $_.FileName -eq "$id.eml" } | + $entry = $entries | + Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } | Select-Object -First 1 if (-not $entry) { Write-Fail "Queue-ID '$id' nicht gefunden." } else { - Retry-FailedQueueEntry -Config $config -Entry$entry + Retry-FailedQueueEntry -Config $config -Entry $entry Write-Ok "[$($entry.QueueId)] zurück nach pending verschoben." } @@ -1621,11 +1622,13 @@ function Manage-FailedQueue { Write-Ok "Failed Queue ist leer." } elseif (Confirm-Yes "Alle $($entries.Count) Failed-Mails erneut zustellen?") { - $ok = 0$failed = 0 + $ok = 0 + $failed = 0 - foreach ($entry in$entries) { + foreach ($entry in $entries) { try { - Retry-FailedQueueEntry -Config $config -Entry $entry$ok++ + Retry-FailedQueueEntry -Config $config -Entry $entry + $ok++ } catch { $failed++ @@ -1645,8 +1648,8 @@ function Manage-FailedQueue { "5" { $id = Read-Host "Queue-ID" - $entry =$entries | - Where-Object { $_.QueueId -eq$id -or $_.FileName -eq$id -or $_.FileName -eq "$id.eml" } | + $entry = $entries | + Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } | Select-Object -First 1 if (-not $entry) { @@ -1668,9 +1671,10 @@ function Manage-FailedQueue { if (Confirm-Yes "Endgültiges Löschen nochmals bestätigen?") { $ok = 0 - foreach ($entry in$entries) { + foreach ($entry in $entries) { try { - Remove-FailedQueueEntry -Entry $entry$ok++ + Remove-FailedQueueEntry -Entry $entry + $ok++ } catch { Write-Warn "[$($entry.QueueId)] konnte nicht gelöscht werden." @@ -1694,19 +1698,19 @@ function Manage-FailedQueue { } } - $config = Get-RelayConfig -TargetPath$InstallPath + $config = Get-RelayConfig -TargetPath $InstallPath } } function Manage-SmtpAuth { - $config = Get-RelayConfig -TargetPath$InstallPath + $config = Get-RelayConfig -TargetPath $InstallPath if (-not $config) { Write-Fail "config.json nicht gefunden." return } - $config = Ensure-SmtpAuthConfig -Config$config + $config = Ensure-SmtpAuthConfig -Config $config while ($true) { Clear-Host @@ -1767,8 +1771,8 @@ function Manage-SmtpAuth { $p1 = Read-Host "Passwort" -AsSecureString $p2 = Read-Host "Passwort wiederholen" -AsSecureString - $plain1 = ConvertTo-PlainText -SecureString$p1 - $plain2 = ConvertTo-PlainText -SecureString$p2 + $plain1 = ConvertTo-PlainText -SecureString $p1 + $plain2 = ConvertTo-PlainText -SecureString $p2 try { if ($plain1.Length -lt 8) { @@ -1936,4 +1940,209 @@ function Manage-SmtpAuth { # Config nach jeder Änderung neu laden, damit Serialisierung/Arrays exakt # dem installierten Zustand entsprechen. - $config = Get-Rel \ No newline at end of file + $config = Get-RelayConfig -TargetPath$InstallPath + $config = Ensure-SmtpAuthConfig -Config$config + } +} + + +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" + + $config = Get-RelayConfig -TargetPath$InstallPath + + Write-Warn "Lokale Deinstallation entfernt Task, Firewallregel und auf Wunsch das lokale Zertifikat." + if (-not (Confirm-Yes "Lokale SMTPGraphRelay-Installation wirklich entfernen?")) { + return + } + + Stop-RelayTask + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + Write-Ok "Scheduled Task entfernt." + + Get-NetFirewallRule -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "SMTPGraphRelay TCP *" } | + Remove-NetFirewallRule -ErrorAction SilentlyContinue + Write-Ok "SMTPGraphRelay Firewallregeln entfernt." + + if ($config -and$config.Graph.CertificateThumbprint) { + if (Confirm-Yes "Lokales Relay-Zertifikat $($config.Graph.CertificateThumbprint) entfernen?") { + Remove-Item -LiteralPath "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)" -Force -ErrorAction SilentlyContinue + Write-Ok "Lokales Zertifikat entfernt." + } + } + + if ($config -and (Confirm-Yes "Auch Entra-App entfernen?")) { + Ensure-Modules + + Write-Warn "Cloud-Cleanup ist destruktiv und betrifft die konfigurierte ClientId:" + Write-Host " $($config.Graph.ClientId)" -ForegroundColor Yellow + + if (Confirm-Yes "Cloud-Cleanup endgültig bestätigen?") { + Connect-RelayGraphAdmin -TenantId $config.Graph.TenantId + + try { + $app = Get-MgApplication -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id,appId,displayName" | Select-Object -First 1 + $sp = Get-MgServicePrincipal -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id,appId,displayName" | Select-Object -First 1 + + if ($sp) { + Remove-MgServicePrincipal -ServicePrincipalId $sp.Id -ErrorAction SilentlyContinue + Write-Ok "Entra Service Principal entfernt." + } + + if ($app) { + Remove-MgApplication -ApplicationId $app.Id -ErrorAction SilentlyContinue + Write-Ok "Entra App Registration entfernt." + } + } + finally { + Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null + } + } + } + + # Programmdateien. Wenn der Installer selbst aus dem Zielordner läuft, kann er + # sich nicht zuverlässig selbst löschen. Dann bleiben Setup + Ordner bis nach Ende stehen. + $currentInstaller = [IO.Path]::GetFullPath($PSCommandPath) + $targetFull = [IO.Path]::GetFullPath($InstallPath) + + foreach ($item in Get-ChildItem -LiteralPath$InstallPath -Force -ErrorAction SilentlyContinue) { + try { + if ($item.FullName -eq$currentInstaller) { + continue + } + + Remove-Item -LiteralPath $item.FullName -Recurse -Force -ErrorAction Stop + } + catch { + Write-Warn "Konnte nicht entfernen: $($item.FullName)" + } + } + + Write-Ok "Lokale Programmdateien entfernt." + if ($currentInstaller.StartsWith($targetFull, [StringComparison]::OrdinalIgnoreCase)) { + Write-Warn "Der aktuell laufende Installer bleibt übrig. Nach dem Beenden kann '$InstallPath' manuell gelöscht werden." + } + + Write-Title "Deinstallation abgeschlossen" +} + +function Show-Status { + Write-Title "SMTPGraphRelay - Status" + + $configPath = Join-Path $InstallPath$ConfigFileName + Write-Host "Installationspfad: $InstallPath" + + if (Test-Path -LiteralPath $configPath) { + Write-Ok "config.json vorhanden." + try { + $config = Get-RelayConfig -TargetPath$InstallPath + Write-Host " Sender: $($config.Graph.SenderMailbox) (ForceSender: $($config.Graph.ForceSender))" + Write-Host " SMTP: $($config.Smtp.ListenAddress):$($config.Smtp.Port)" + Write-Host " Client: $($config.Graph.ClientId)" + + if ($config.Smtp.PSObject.Properties.Name -contains "RequireAuth") { + $authText = if ([bool]$config.Smtp.RequireAuth) { "erforderlich" } else { "optional / aus" } + $authUsers = if ($config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { @($config.Smtp.AuthUsers).Count } else { 0 } + Write-Host " AUTH: $authText ($authUsers Benutzer)" + } + + try { + $failedCount = @(Get-FailedQueueEntries -Config$config).Count + Write-Host " Failed: $failedCount Mail(s)" + } catch {} + } catch {} + } + else { + Write-Warn "Keine config.json vorhanden." + } + + $task = Get-ScheduledTask -TaskName$TaskName -ErrorAction SilentlyContinue + if ($task) { + Write-Host "Task State: $($task.State)" + } + else { + Write-Warn "Scheduled Task nicht vorhanden." + } +} + +function Show-Menu { + Clear-Host + Write-Title "SMTPGraphRelay - Bootstrap / Repair / Online Update" + Write-Host "Installationspfad: $InstallPath" -ForegroundColor DarkGray + + $installedVersion = Get-InstalledVersion -TargetPath$InstallPath + + if ($installedVersion -and$installedVersion.Version) { + Write-Host "Installiert: $($installedVersion.Version)" -ForegroundColor DarkGray + } + + Write-Host "Updatequelle: Gitea / main" -ForegroundColor DarkGray + Write-Host "" + Write-Host " [1] Neuinstallation aus Gitea" + Write-Host " [2] Installation aus Gitea reparieren" + Write-Host " [3] Nach Online-Updates suchen" + Write-Host " [4] Entra App & Berechtigungen prüfen" + Write-Host " [5] Zertifikat erneuern" + Write-Host " [6] Health Check ausführen" + Write-Host " [7] Deinstallieren" + 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 "" +} + +Assert-WindowsPowerShell51 + +while ($true) { + Show-Menu + $choice = Read-Host "Auswahl" + + try { + switch ($choice) { + "1" { Install-New } + "2" { Repair-Installation } + "3" { Update-Relay } + "4" { Verify-CloudPermissions } + "5" { Invoke-CertificateRenewal } + "6" { Invoke-HealthCheck } + "7" { Uninstall-Relay } + "8" { Show-Status } + "9" { Manage-SmtpAuth } + "10" { Manage-FailedQueue } + "11" { Invoke-FailureModeTests } + "0" { break } + default { Write-Warn "Ungültige Auswahl." } + } + } + catch { + Write-Host "" + Write-Fail $_.Exception.Message + if ($_.ScriptStackTrace) { + Write-Host $_.ScriptStackTrace -ForegroundColor DarkYellow + } + } + + if ($choice -ne "0") { + Write-Host "" + Read-Host "Enter drücken, um zum Menü zurückzukehren" + } + + if ($choice -eq "0") { + break + } +} \ No newline at end of file