Setup-SMTPGraphRelay.ps1 aktualisiert

This commit is contained in:
manuel.maier committed 2026-10-13 12:06:38 +02:00
1 parent 1998601ba6
commit 4ae9d90390
1 file changed
+93 -94
+93 -94
View File
@@ -5,19 +5,19 @@
SMTPGraphRelay Installer / Repair / Update SMTPGraphRelay Installer / Repair / Update
.DESCRIPTION .DESCRIPTION
Einheitliches Verwaltungswerkzeug für SMTPGraphRelay. Einheitliches Verwaltungswerkzeug fuer SMTPGraphRelay.
Modi: Modi:
1 - Neuinstallation 1 - Neuinstallation
2 - Installation reparieren 2 - Installation reparieren
3 - Relay aktualisieren 3 - Relay aktualisieren
4 - Entra App & Berechtigungen prüfen 4 - Entra App & Berechtigungen pruefen
5 - Zertifikat erneuern 5 - Zertifikat erneuern
6 - Health Check ausführen 6 - Health Check ausfuehren
7 - Deinstallieren 7 - Deinstallieren
WICHTIG: WICHTIG:
Dieses Skript muss mit Windows PowerShell 5.1 ausgeführt werden. Dieses Skript muss mit Windows PowerShell 5.1 ausgefuehrt werden.
#> #>
[CmdletBinding()] [CmdletBinding()]
@@ -41,7 +41,7 @@ $RemoteVersionUrl = "$RepoBaseUrl/raw/branch/main/version.json"
$RemoteArchiveUrl = "$RepoBaseUrl/archive/main.zip" $RemoteArchiveUrl = "$RepoBaseUrl/archive/main.zip"
$RemoteRelayUrl = "$RepoBaseUrl/raw/branch/main/SMTPGraphRelay.ps1" $RemoteRelayUrl = "$RepoBaseUrl/raw/branch/main/SMTPGraphRelay.ps1"
# Dateien, die ein Update verändern darf. # Dateien, die ein Update veraendern darf.
# config.json, Queue, Logs und sonstige lokale Daten sind absichtlich NICHT enthalten. # config.json, Queue, Logs und sonstige lokale Daten sind absichtlich NICHT enthalten.
$ManagedReleaseFiles = @( $ManagedReleaseFiles = @(
"SMTPGraphRelay.ps1", "SMTPGraphRelay.ps1",
@@ -83,7 +83,7 @@ function Confirm-Yes {
function Assert-WindowsPowerShell51 { function Assert-WindowsPowerShell51 {
if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) { if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) {
Write-Title "FALSCHE POWERSHELL-VERSION" Write-Title "FALSCHE POWERSHELL-VERSION"
Write-Fail "Dieses Tool muss mit Windows PowerShell 5.1 ausgeführt werden." Write-Fail "Dieses Tool muss mit Windows PowerShell 5.1 ausgefuehrt werden."
Write-Host "" Write-Host ""
Write-Host "Aktuell erkannt:" Write-Host "Aktuell erkannt:"
Write-Host " Edition: $($PSVersionTable.PSEdition)" Write-Host " Edition: $($PSVersionTable.PSEdition)"
@@ -116,31 +116,30 @@ function Ensure-Modules {
"Microsoft.Graph.Applications" "Microsoft.Graph.Applications"
) )
$allUsersPath = Join-Path $env:ProgramFiles "WindowsPowerShell\Modules"
foreach ($module in $modules) { foreach ($module in $modules) {
# Pruefe explizit, ob das Modul im AllUsers-Pfad liegt
$existing = Get-Module -ListAvailable -Name $module | $existing = Get-Module -ListAvailable -Name $module |
Where-Object { $_.ModuleBase -like "$allUsersPath*" } |
Sort-Object Version -Descending | Sort-Object Version -Descending |
Select-Object -First 1 Select-Object -First 1
if (-not $existing) { if (-not $existing) {
Write-Info "$module fehlt. Installation für AllUsers..." Write-Info "$module fehlt (System-weit). Installation fuer AllUsers..."
Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber
$existing = Get-Module -ListAvailable -Name $module | $existing = Get-Module -ListAvailable -Name $module |
Where-Object { $_.ModuleBase -like "$allUsersPath*" } |
Sort-Object Version -Descending | Sort-Object Version -Descending |
Select-Object -First 1 Select-Object -First 1
} }
if (-not $existing) { if (-not $existing) {
throw "Modul '$module' konnte nicht installiert/gefunden werden." throw "Modul '$module' konnte nicht fuer AllUsers installiert/gefunden werden."
} }
# Schutz gegen den bereits beobachteten PowerShell-7-Pfad. Write-Ok "$module verfuegbar (System-weit)."
if ($existing.ModuleBase -notmatch '\\WindowsPowerShell\\Modules\\') {
Write-Warn "$module wurde gefunden, aber nicht im Windows-PowerShell-Modulpfad: $($existing.ModuleBase)"
Write-Info "Installiere das Modul nochmals explizit aus Windows PowerShell 5.1..."
Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber
}
Write-Ok "$module verfügbar."
} }
} }
@@ -175,10 +174,10 @@ function New-RepoStagingArea {
$releaseRoot = Find-ExtractedReleaseRoot -ExtractPath $extract $releaseRoot = Find-ExtractedReleaseRoot -ExtractPath $extract
# Pflichtdateien prüfen. # Pflichtdateien pruefen.
foreach ($required in @("SMTPGraphRelay.ps1", "version.json")) { foreach ($required in @("SMTPGraphRelay.ps1", "version.json")) {
if (-not (Test-Path -LiteralPath (Join-Path $releaseRoot $required))) { if (-not (Test-Path -LiteralPath (Join-Path $releaseRoot $required))) {
throw "Repository-Archiv ist unvollständig: '$required' fehlt." throw "Repository-Archiv ist unvollstaendig: '$required' fehlt."
} }
} }
@@ -188,7 +187,7 @@ function New-RepoStagingArea {
-Encoding UTF8 | ConvertFrom-Json -Encoding UTF8 | ConvertFrom-Json
if (-not $versionInfo.Version) { if (-not $versionInfo.Version) {
throw "version.json aus dem Repository enthält keine Version." throw "version.json aus dem Repository enthaelt keine Version."
} }
Write-Ok "Repository-Version $($versionInfo.Version) geladen." Write-Ok "Repository-Version $($versionInfo.Version) geladen."
@@ -562,7 +561,7 @@ function Get-RemoteVersion {
$remote = Get-Content -LiteralPath $tempFile -Raw -Encoding UTF8 | ConvertFrom-Json $remote = Get-Content -LiteralPath $tempFile -Raw -Encoding UTF8 | ConvertFrom-Json
if (-not $remote.Version) { if (-not $remote.Version) {
throw "Remote version.json enthält keine Version." throw "Remote version.json enthaelt keine Version."
} }
return $remote return $remote
@@ -596,7 +595,7 @@ function Find-ExtractedReleaseRoot {
[Parameter(Mandatory)][string]$ExtractPath [Parameter(Mandatory)][string]$ExtractPath
) )
# Gitea kann beim Archiv einen zusätzlichen Root-Ordner erzeugen. # Gitea kann beim Archiv einen zusaetzlichen Root-Ordner erzeugen.
# Daher suchen wir nach der Kombination aus Relay + version.json statt # Daher suchen wir nach der Kombination aus Relay + version.json statt
# einen konkreten Archivordnernamen vorauszusetzen. # einen konkreten Archivordnernamen vorauszusetzen.
$relayFiles = Get-ChildItem ` $relayFiles = Get-ChildItem `
@@ -613,7 +612,7 @@ function Find-ExtractedReleaseRoot {
} }
} }
throw "Im heruntergeladenen Archiv wurde kein gültiges SMTPGraphRelay-Release gefunden." throw "Im heruntergeladenen Archiv wurde kein gueltiges SMTPGraphRelay-Release gefunden."
} }
function Backup-ManagedFiles { function Backup-ManagedFiles {
@@ -666,7 +665,7 @@ function Install-ExtractedRelease {
foreach ($name in $required) { foreach ($name in $required) {
if (-not (Test-Path -LiteralPath (Join-Path $ReleaseRoot $name))) { if (-not (Test-Path -LiteralPath (Join-Path $ReleaseRoot $name))) {
throw "Updatepaket ist unvollständig: '$name' fehlt." throw "Updatepaket ist unvollstaendig: '$name' fehlt."
} }
} }
@@ -688,7 +687,7 @@ function Invoke-PostUpdateHealthCheck {
$health = Join-Path $TargetPath $HealthFileName $health = Join-Path $TargetPath $HealthFileName
if (-not (Test-Path -LiteralPath $health)) { if (-not (Test-Path -LiteralPath $health)) {
Write-Warn "Health Check ist nicht installiert; automatische Nachprüfung entfällt." Write-Warn "Health Check ist nicht installiert; automatische Nachpruefung entfaellt."
return 0 return 0
} }
@@ -703,7 +702,7 @@ function Install-New {
if (Test-Path -LiteralPath (Join-Path $InstallPath $ConfigFileName)) { if (Test-Path -LiteralPath (Join-Path $InstallPath $ConfigFileName)) {
Write-Warn "Es existiert bereits eine config.json unter:" Write-Warn "Es existiert bereits eine config.json unter:"
Write-Host " $InstallPath" Write-Host " $InstallPath"
Write-Warn "Neuinstallation würde eine neue App/Zertifikat erzeugen." Write-Warn "Neuinstallation wuerde eine neue App/Zertifikat erzeugen."
if (-not (Confirm-Yes "Trotzdem fortfahren?")) { if (-not (Confirm-Yes "Trotzdem fortfahren?")) {
return return
@@ -718,7 +717,7 @@ function Install-New {
Ensure-Modules Ensure-Modules
Ensure-Directories -TargetPath $InstallPath Ensure-Directories -TargetPath $InstallPath
# Nur Programmdateien aus Git übernehmen. # Nur Programmdateien aus Git uebernehmen.
Install-RepoProgramFiles ` Install-RepoProgramFiles `
-ReleaseRoot $staging.ReleaseRoot ` -ReleaseRoot $staging.ReleaseRoot `
-TargetPath $InstallPath -TargetPath $InstallPath
@@ -729,7 +728,7 @@ function Install-New {
$senderMailbox = Read-Host "Standard-Absenderpostfach (Fallback/Test, z.B. info@firma.de)" $senderMailbox = Read-Host "Standard-Absenderpostfach (Fallback/Test, z.B. info@firma.de)"
while ([string]::IsNullOrWhiteSpace($senderMailbox) -or $senderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') { while ([string]::IsNullOrWhiteSpace($senderMailbox) -or $senderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') {
$senderMailbox = Read-Host "Bitte eine gültige Mailadresse eingeben" $senderMailbox = Read-Host "Bitte eine gueltige Mailadresse eingeben"
} }
$listenAddress = Read-Default "Lokale Listen-IP" "0.0.0.0" $listenAddress = Read-Default "Lokale Listen-IP" "0.0.0.0"
@@ -791,7 +790,7 @@ function Install-New {
-PrincipalId $sp.Id ` -PrincipalId $sp.Id `
-ResourceId $graphSp.Id ` -ResourceId $graphSp.Id `
-AppRoleId $mailSendRole.Id | Out-Null -AppRoleId $mailSendRole.Id | Out-Null
Write-Ok "Globale 'Mail.Send' Application Role (ohne Einschränkung) erfolgreich vergeben." Write-Ok "Globale 'Mail.Send' Application Role (ohne Einschraenkung) erfolgreich vergeben."
} else { } else {
Write-Ok "Globale 'Mail.Send' Berechtigung war bereits vergeben." Write-Ok "Globale 'Mail.Send' Berechtigung war bereits vergeben."
} }
@@ -861,7 +860,7 @@ function Install-New {
$ctx = Get-MgContext $ctx = Get-MgContext
if (-not $ctx -or $ctx.AuthType -ne "AppOnly") { if (-not $ctx -or $ctx.AuthType -ne "AppOnly") {
throw "App-only Anmeldung konnte nicht bestätigt werden." throw "App-only Anmeldung konnte nicht bestaetigt werden."
} }
Write-Ok "App-only Anmeldung funktioniert." Write-Ok "App-only Anmeldung funktioniert."
@@ -871,12 +870,12 @@ function Install-New {
$health = Join-Path $InstallPath $HealthFileName $health = Join-Path $InstallPath $HealthFileName
if (Test-Path -LiteralPath $health) { if (Test-Path -LiteralPath $health) {
Write-Info "Starte abschließenden Health Check..." Write-Info "Starte abschliessenden Health Check..."
& $health -ConfigPath (Join-Path $InstallPath $ConfigFileName) & $health -ConfigPath (Join-Path $InstallPath $ConfigFileName)
$healthExit = $LASTEXITCODE $healthExit = $LASTEXITCODE
if ($healthExit -ge 2) { if ($healthExit -ge 2) {
Write-Warn "Installation abgeschlossen, Health Check meldet Fehler. Bitte Ausgabe prüfen." Write-Warn "Installation abgeschlossen, Health Check meldet Fehler. Bitte Ausgabe pruefen."
} }
} }
@@ -903,7 +902,7 @@ function Repair-Installation {
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath $InstallPath
if (-not $config) { if (-not $config) {
Write-Fail "Keine config.json gefunden. Repair ist nur für bestehende Installationen gedacht." Write-Fail "Keine config.json gefunden. Repair ist nur fuer bestehende Installationen gedacht."
Write-Info "Bitte Neuinstallation verwenden." Write-Info "Bitte Neuinstallation verwenden."
return return
} }
@@ -925,7 +924,7 @@ function Repair-Installation {
Write-Warn "Repair erzeugt absichtlich kein neues Credential. Nutze Zertifikat erneuern." Write-Warn "Repair erzeugt absichtlich kein neues Credential. Nutze Zertifikat erneuern."
} }
elseif (-not $cert.HasPrivateKey) { elseif (-not $cert.HasPrivateKey) {
Write-Fail "Konfiguriertes Zertifikat besitzt keinen privaten Schlüssel." Write-Fail "Konfiguriertes Zertifikat besitzt keinen privaten Schluessel."
} }
else { else {
Write-Ok "Zertifikat vorhanden und besitzt Private Key." Write-Ok "Zertifikat vorhanden und besitzt Private Key."
@@ -952,7 +951,7 @@ function Repair-Installation {
} }
if ($healthExit -eq 1) { if ($healthExit -eq 1) {
Write-Warn "Repair abgeschlossen, Health Check enthält Warnungen." Write-Warn "Repair abgeschlossen, Health Check enthaelt Warnungen."
} }
else { else {
Write-Ok "Repair Health Check erfolgreich." Write-Ok "Repair Health Check erfolgreich."
@@ -990,7 +989,7 @@ function Update-Relay {
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath $InstallPath
if (-not $config) { if (-not $config) {
Write-Fail "Keine bestehende config.json gefunden." Write-Fail "Keine bestehende config.json gefunden."
Write-Info "Für eine neue Installation bitte 'Neuinstallation' wählen." Write-Info "Fuer eine neue Installation bitte 'Neuinstallation' waehlen."
return return
} }
@@ -1005,12 +1004,12 @@ function Update-Relay {
Write-Warn "Keine installierte version.json gefunden." Write-Warn "Keine installierte version.json gefunden."
$installedVersion = Read-Host "Installierte Version manuell eingeben (z.B. 1.5.0)" $installedVersion = Read-Host "Installierte Version manuell eingeben (z.B. 1.5.0)"
if ([string]::IsNullOrWhiteSpace($installedVersion)) { if ([string]::IsNullOrWhiteSpace($installedVersion)) {
Write-Fail "Ohne lokale Versionsinformation kann kein sicheres Online-Update durchgeführt werden." Write-Fail "Ohne lokale Versionsinformation kann kein sicheres Online-Update durchgefuehrt werden."
return return
} }
} }
Write-Info "Prüfe Gitea auf neue Version..." Write-Info "Pruefe Gitea auf neue Version..."
Write-Info "Repository: $RepoBaseUrl" Write-Info "Repository: $RepoBaseUrl"
try { try {
@@ -1039,13 +1038,13 @@ function Update-Relay {
if ($comparison -lt 0) { if ($comparison -lt 0) {
Write-Warn "Die installierte Version ($installedVersion) ist neuer als main ($remoteVersion)." Write-Warn "Die installierte Version ($installedVersion) ist neuer als main ($remoteVersion)."
if (-not (Confirm-Yes "Downgrade auf $remoteVersion durchführen?")) { if (-not (Confirm-Yes "Downgrade auf $remoteVersion durchfuehren?")) {
return return
} }
} }
else { else {
Write-Host "" Write-Host ""
Write-Host "Update verfügbar:" -ForegroundColor Green Write-Host "Update verfuegbar:" -ForegroundColor Green
Write-Host " Installiert: $installedVersion" Write-Host " Installiert: $installedVersion"
Write-Host " Neu: $remoteVersion" -ForegroundColor Yellow Write-Host " Neu: $remoteVersion" -ForegroundColor Yellow
Write-Host "" Write-Host ""
@@ -1096,11 +1095,11 @@ function Update-Relay {
-Encoding UTF8 | ConvertFrom-Json -Encoding UTF8 | ConvertFrom-Json
if (-not $downloadedVersionInfo.Version) { if (-not $downloadedVersionInfo.Version) {
throw "version.json im Archiv enthält keine Version." throw "version.json im Archiv enthaelt keine Version."
} }
if ([string]$downloadedVersionInfo.Version -ne $remoteVersion) { if ([string]$downloadedVersionInfo.Version -ne $remoteVersion) {
throw "Versionskonflikt: version.json-URL meldet $remoteVersion, Archiv enthält $($downloadedVersionInfo.Version)." throw "Versionskonflikt: version.json-URL meldet $remoteVersion, Archiv enthaelt $($downloadedVersionInfo.Version)."
} }
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
@@ -1132,7 +1131,7 @@ function Update-Relay {
} }
if ($healthExit -eq 1) { if ($healthExit -eq 1) {
Write-Warn "Update erfolgreich, Health Check enthält Warnungen." Write-Warn "Update erfolgreich, Health Check enthaelt Warnungen."
} }
else { else {
Write-Ok "Health Check nach Update erfolgreich." Write-Ok "Health Check nach Update erfolgreich."
@@ -1148,7 +1147,7 @@ function Update-Relay {
Write-Fail "Update fehlgeschlagen: $($_.Exception.Message)" Write-Fail "Update fehlgeschlagen: $($_.Exception.Message)"
if ($backupPath -and (Test-Path -LiteralPath $backupPath)) { if ($backupPath -and (Test-Path -LiteralPath $backupPath)) {
Write-Warn "Automatischer Rollback wird durchgeführt..." Write-Warn "Automatischer Rollback wird durchgefuehrt..."
try { try {
Stop-RelayTask Stop-RelayTask
@@ -1173,7 +1172,7 @@ function Update-Relay {
} }
function Verify-CloudPermissions { function Verify-CloudPermissions {
Write-Title "SMTPGraphRelay - Entra Berechtigungen prüfen" Write-Title "SMTPGraphRelay - Entra Berechtigungen pruefen"
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath $InstallPath
if (-not $config) { if (-not $config) {
@@ -1215,7 +1214,7 @@ function Verify-CloudPermissions {
if ($globalAssig) { if ($globalAssig) {
Write-Ok "Erfolg: App besitzt die erforderliche GLOBALE (tenant-weite) Microsoft Graph Mail.Send Berechtigung in Entra ID." Write-Ok "Erfolg: App besitzt die erforderliche GLOBALE (tenant-weite) Microsoft Graph Mail.Send Berechtigung in Entra ID."
} else { } else {
Write-Fail "Fehler: Der App fehlt die GLOBALE Microsoft Graph Mail.Send Berechtigung. Bitte in Entra ID hinzufügen und zustimmen." Write-Fail "Fehler: Der App fehlt die GLOBALE Microsoft Graph Mail.Send Berechtigung. Bitte in Entra ID hinzufuegen und zustimmen."
} }
} }
finally { finally {
@@ -1236,7 +1235,7 @@ function Invoke-CertificateRenewal {
} }
function Invoke-HealthCheck { function Invoke-HealthCheck {
Write-Title "SMTPGraphRelay - Health Check" Write-Title "SMTPGraphRelay - Health Check ausfuehren"
$health = Join-Path $InstallPath $HealthFileName $health = Join-Path $InstallPath $HealthFileName
if (-not (Test-Path -LiteralPath $health)) { if (-not (Test-Path -LiteralPath $health)) {
@@ -1273,7 +1272,7 @@ function Invoke-Pbkdf2Sha256 {
throw "Iterations must be greater than zero." throw "Iterations must be greater than zero."
} }
# Auf unterstützten .NET-Framework-Versionen verwenden wir die native, # Auf unterstuetzten .NET-Framework-Versionen verwenden wir die native,
# schnelle PBKDF2-SHA256-Implementierung. # schnelle PBKDF2-SHA256-Implementierung.
try { try {
$derive = New-Object System.Security.Cryptography.Rfc2898DeriveBytes( $derive = New-Object System.Security.Cryptography.Rfc2898DeriveBytes(
@@ -1291,7 +1290,7 @@ function Invoke-Pbkdf2Sha256 {
} }
} }
catch { catch {
# Kompatibilitäts-Fallback für ältere .NET-Framework-Stände. # Kompatibilitaets-Fallback fuer aeltere .NET-Framework-Staende.
$hmac = New-Object System.Security.Cryptography.HMACSHA256 $hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [Text.Encoding]::UTF8.GetBytes($Password) $hmac.Key = [Text.Encoding]::UTF8.GetBytes($Password)
@@ -1487,7 +1486,7 @@ function Retry-FailedQueueEntry {
$targetMeta = "$targetEml.json" $targetMeta = "$targetEml.json"
if (Test-Path -LiteralPath $targetEml) { if (Test-Path -LiteralPath $targetEml) {
throw "Pending enthält bereits '$($Entry.FileName)'." throw "Pending enthaelt bereits '$($Entry.FileName)'."
} }
if ($Entry.Meta) { if ($Entry.Meta) {
@@ -1551,9 +1550,9 @@ function Manage-FailedQueue {
Write-Host " [2] Details einer Mail anzeigen" Write-Host " [2] Details einer Mail anzeigen"
Write-Host " [3] Eine Mail erneut zustellen" Write-Host " [3] Eine Mail erneut zustellen"
Write-Host " [4] Alle Mails erneut zustellen" Write-Host " [4] Alle Mails erneut zustellen"
Write-Host " [5] Eine Mail endgültig löschen" Write-Host " [5] Eine Mail endgueltig loeschen"
Write-Host " [6] Alle Failed-Mails endgültig löschen" Write-Host " [6] Alle Failed-Mails endgueltig loeschen"
Write-Host " [0] Zurück" Write-Host " [0] Zurueck"
Write-Host "" Write-Host ""
$choice = Read-Host "Auswahl" $choice = Read-Host "Auswahl"
@@ -1592,7 +1591,7 @@ function Manage-FailedQueue {
Write-Host "An: $($entry.Recipients)" Write-Host "An: $($entry.Recipients)"
Write-Host "Auth-User: $($entry.AuthenticatedUser)" Write-Host "Auth-User: $($entry.AuthenticatedUser)"
Write-Host "Status: $($entry.LastStatusCode)" Write-Host "Status: $($entry.LastStatusCode)"
Write-Host "Größe: $($entry.SizeKB) KB" Write-Host "Groesse: $($entry.SizeKB) KB"
Write-Host "" Write-Host ""
Write-Host "Letzter Fehler:" -ForegroundColor Yellow Write-Host "Letzter Fehler:" -ForegroundColor Yellow
Write-Host $entry.LastError Write-Host $entry.LastError
@@ -1613,7 +1612,7 @@ function Manage-FailedQueue {
} }
else { else {
Retry-FailedQueueEntry -Config $config -Entry $entry Retry-FailedQueueEntry -Config $config -Entry $entry
Write-Ok "[$($entry.QueueId)] zurück nach pending verschoben." Write-Ok "[$($entry.QueueId)] zurueck nach pending verschoben."
} }
Read-Host "Enter" Read-Host "Enter"
@@ -1638,7 +1637,7 @@ function Manage-FailedQueue {
} }
} }
Write-Ok "$ok Mail(s) zurück nach pending verschoben." Write-Ok "$ok Mail(s) zurueck nach pending verschoben."
if ($failed -gt 0) { if ($failed -gt 0) {
Write-Warn "$failed Mail(s) konnten nicht verschoben werden." Write-Warn "$failed Mail(s) konnten nicht verschoben werden."
} }
@@ -1657,9 +1656,9 @@ function Manage-FailedQueue {
if (-not $entry) { if (-not $entry) {
Write-Fail "Queue-ID '$id' nicht gefunden." Write-Fail "Queue-ID '$id' nicht gefunden."
} }
elseif (Confirm-Yes "[$($entry.QueueId)] endgültig aus Failed löschen?") { elseif (Confirm-Yes "[$($entry.QueueId)] endgueltig aus Failed loeschen?") {
Remove-FailedQueueEntry -Entry $entry Remove-FailedQueueEntry -Entry $entry
Write-Ok "[$($entry.QueueId)] gelöscht." Write-Ok "[$($entry.QueueId)] geloescht."
} }
Read-Host "Enter" Read-Host "Enter"
@@ -1669,8 +1668,8 @@ function Manage-FailedQueue {
if ($entries.Count -eq 0) { if ($entries.Count -eq 0) {
Write-Ok "Failed Queue ist leer." Write-Ok "Failed Queue ist leer."
} }
elseif (Confirm-Yes "WIRKLICH alle $($entries.Count) Failed-Mails endgültig löschen?") { elseif (Confirm-Yes "WIRKLICH alle $($entries.Count) Failed-Mails endgueltig loeschen?") {
if (Confirm-Yes "Endgültiges Löschen nochmals bestätigen?") { if (Confirm-Yes "Endgueltiges Loeschen nochmals bestaetigen?") {
$ok = 0 $ok = 0
foreach ($entry in $entries) { foreach ($entry in $entries) {
@@ -1679,11 +1678,11 @@ function Manage-FailedQueue {
$ok++ $ok++
} }
catch { catch {
Write-Warn "[$($entry.QueueId)] konnte nicht gelöscht werden." Write-Warn "[$($entry.QueueId)] konnte nicht geloescht werden."
} }
} }
Write-Ok "$ok Failed-Mail(s) endgültig gelöscht." Write-Ok "$ok Failed-Mail(s) endgueltig geloescht."
} }
} }
@@ -1695,7 +1694,7 @@ function Manage-FailedQueue {
} }
default { default {
Write-Warn "Ungültige Auswahl." Write-Warn "Ungueltige Auswahl."
Start-Sleep -Seconds 1 Start-Sleep -Seconds 1
} }
} }
@@ -1727,13 +1726,13 @@ function Manage-SmtpAuth {
Write-Host "Ohne Auth erlaubte Netze: $(@($config.Smtp.AllowUnauthenticatedNetworks) -join ', ')" Write-Host "Ohne Auth erlaubte Netze: $(@($config.Smtp.AllowUnauthenticatedNetworks) -join ', ')"
Write-Host "" Write-Host ""
Write-Host " [1] SMTP-AUTH erforderlich EIN/AUS" Write-Host " [1] SMTP-AUTH erforderlich EIN/AUS"
Write-Host " [2] Benutzer hinzufügen" Write-Host " [2] Benutzer hinzufuegen"
Write-Host " [3] Benutzer anzeigen" Write-Host " [3] Benutzer anzeigen"
Write-Host " [4] Passwort ändern" Write-Host " [4] Passwort aendern"
Write-Host " [5] Benutzer löschen" Write-Host " [5] Benutzer loeschen"
Write-Host " [6] Netze ohne Auth verwalten" Write-Host " [6] Netze ohne Auth verwalten"
Write-Host " [7] Max. Fehlversuche ändern" Write-Host " [7] Max. Fehlversuche aendern"
Write-Host " [0] Zurück" Write-Host " [0] Zurueck"
Write-Host "" Write-Host ""
$choice = Read-Host "Auswahl" $choice = Read-Host "Auswahl"
@@ -1747,7 +1746,7 @@ function Manage-SmtpAuth {
} }
Save-SmtpAuthConfigAndRestart -Config $config Save-SmtpAuthConfigAndRestart -Config $config
Write-Ok "SMTP-AUTH Status geändert." Write-Ok "SMTP-AUTH Status geaendert."
Read-Host "Enter" Read-Host "Enter"
} }
@@ -1755,7 +1754,7 @@ function Manage-SmtpAuth {
$username = Read-Host "Benutzername" $username = Read-Host "Benutzername"
if ([string]::IsNullOrWhiteSpace($username) -or $username -notmatch '^[A-Za-z0-9._@-]{1,128}$') { if ([string]::IsNullOrWhiteSpace($username) -or $username -notmatch '^[A-Za-z0-9._@-]{1,128}$') {
Write-Fail "Ungültiger Benutzername." Write-Fail "Ungueltiger Benutzername."
Read-Host "Enter" Read-Host "Enter"
continue continue
} }
@@ -1773,8 +1772,8 @@ function Manage-SmtpAuth {
$p1 = Read-Host "Passwort" -AsSecureString $p1 = Read-Host "Passwort" -AsSecureString
$p2 = Read-Host "Passwort wiederholen" -AsSecureString $p2 = Read-Host "Passwort wiederholen" -AsSecureString
$plain1 = ConvertTo-PlainText -SecureString $p1 $plain1 = ConvertTo-PlainText -SecureString$p1
$plain2 = ConvertTo-PlainText -SecureString $p2 $plain2 = ConvertTo-PlainText -SecureString$p2
try { try {
if ($plain1.Length -lt 8) { if ($plain1.Length -lt 8) {
@@ -1784,7 +1783,7 @@ function Manage-SmtpAuth {
} }
if ($plain1 -cne$plain2) { if ($plain1 -cne$plain2) {
Write-Fail "Passwörter stimmen nicht überein." Write-Fail "Passwoerter stimmen nicht ueberein."
Read-Host "Enter" Read-Host "Enter"
continue continue
} }
@@ -1852,7 +1851,7 @@ function Manage-SmtpAuth {
} }
if ($plain1 -cne$plain2) { if ($plain1 -cne$plain2) {
Write-Fail "Passwörter stimmen nicht überein." Write-Fail "Passwoerter stimmen nicht ueberein."
Read-Host "Enter" Read-Host "Enter"
continue continue
} }
@@ -1868,7 +1867,7 @@ function Manage-SmtpAuth {
$user.Iterations =$record.Iterations $user.Iterations =$record.Iterations
Save-SmtpAuthConfigAndRestart -Config $config Save-SmtpAuthConfigAndRestart -Config $config
Write-Ok "Passwort für '$username' geändert." Write-Ok "Passwort fuer '$username' geaendert."
Read-Host "Enter" Read-Host "Enter"
} }
@@ -1884,13 +1883,13 @@ function Manage-SmtpAuth {
continue continue
} }
if (Confirm-Yes "Benutzer '$username' wirklich löschen?") { if (Confirm-Yes "Benutzer '$username' wirklich loeschen?") {
$config.Smtp.AuthUsers = @($config.Smtp.AuthUsers | $config.Smtp.AuthUsers = @($config.Smtp.AuthUsers |
Where-Object { -not ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) } Where-Object { -not ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) }
) )
Save-SmtpAuthConfigAndRestart -Config $config Save-SmtpAuthConfigAndRestart -Config $config
Write-Ok "Benutzer '$username' gelöscht." Write-Ok "Benutzer '$username' geloescht."
} }
Read-Host "Enter" Read-Host "Enter"
@@ -1898,7 +1897,7 @@ function Manage-SmtpAuth {
"6" { "6" {
$current = @($config.Smtp.AllowUnauthenticatedNetworks) -join "," $current = @($config.Smtp.AllowUnauthenticatedNetworks) -join ","
$input = Read-Default "Netze/IPs ohne AUTH, Komma getrennt; '-' für keine" $(if ($current) {$current } else { "-" }) $input = Read-Default "Netze/IPs ohne AUTH, Komma getrennt; '-' fuer keine" $(if ($current) {$current } else { "-" })
if ($input -eq "-") { if ($input -eq "-") {
$config.Smtp.AllowUnauthenticatedNetworks = @() $config.Smtp.AllowUnauthenticatedNetworks = @()
@@ -1911,7 +1910,7 @@ function Manage-SmtpAuth {
} }
Save-SmtpAuthConfigAndRestart -Config $config Save-SmtpAuthConfigAndRestart -Config $config
Write-Ok "Ausnahmen für SMTP-AUTH gespeichert." Write-Ok "Ausnahmen fuer SMTP-AUTH gespeichert."
Read-Host "Enter" Read-Host "Enter"
} }
@@ -1921,7 +1920,7 @@ function Manage-SmtpAuth {
if ($value -match '^\d+$' -and [int]$value -ge 1 -and [int]$value -le 100) { if ($value -match '^\d+$' -and [int]$value -ge 1 -and [int]$value -le 100) {
$config.Smtp.AuthMaxFailures = [int]$value $config.Smtp.AuthMaxFailures = [int]$value
Save-SmtpAuthConfigAndRestart -Config $config Save-SmtpAuthConfigAndRestart -Config $config
Write-Ok "Maximale Fehlversuche geändert." Write-Ok "Maximale Fehlversuche geaendert."
} }
else { else {
Write-Fail "Bitte einen Wert zwischen 1 und 100 eingeben." Write-Fail "Bitte einen Wert zwischen 1 und 100 eingeben."
@@ -1935,14 +1934,14 @@ function Manage-SmtpAuth {
} }
default { default {
Write-Warn "Ungültige Auswahl." Write-Warn "Ungueltige Auswahl."
Start-Sleep -Seconds 1 Start-Sleep -Seconds 1
} }
} }
# Config nach jeder Änderung neu laden, damit Serialisierung/Arrays exakt # Config nach jeder Aenderung neu laden, damit Serialisierung/Arrays exakt
# dem installierten Zustand entsprechen. # dem installierten Zustand entsprechen.
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath$InstallPath
$config = Ensure-SmtpAuthConfig -Config$config $config = Ensure-SmtpAuthConfig -Config$config
} }
} }
@@ -1963,7 +1962,7 @@ function Invoke-FailureModeTests {
function Uninstall-Relay { function Uninstall-Relay {
Write-Title "SMTPGraphRelay - Deinstallation" Write-Title "SMTPGraphRelay - Deinstallation"
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath$InstallPath
Write-Warn "Lokale Deinstallation entfernt Task, Firewallregel und auf Wunsch das lokale Zertifikat." Write-Warn "Lokale Deinstallation entfernt Task, Firewallregel und auf Wunsch das lokale Zertifikat."
if (-not (Confirm-Yes "Lokale SMTPGraphRelay-Installation wirklich entfernen?")) { if (-not (Confirm-Yes "Lokale SMTPGraphRelay-Installation wirklich entfernen?")) {
@@ -1992,7 +1991,7 @@ function Uninstall-Relay {
Write-Warn "Cloud-Cleanup ist destruktiv und betrifft die konfigurierte ClientId:" Write-Warn "Cloud-Cleanup ist destruktiv und betrifft die konfigurierte ClientId:"
Write-Host " $($config.Graph.ClientId)" -ForegroundColor Yellow Write-Host " $($config.Graph.ClientId)" -ForegroundColor Yellow
if (Confirm-Yes "Cloud-Cleanup endgültig bestätigen?") { if (Confirm-Yes "Cloud-Cleanup endgueltig bestaetigen?") {
Connect-RelayGraphAdmin -TenantId $config.Graph.TenantId Connect-RelayGraphAdmin -TenantId $config.Graph.TenantId
try { try {
@@ -2015,8 +2014,8 @@ function Uninstall-Relay {
} }
} }
# Programmdateien. Wenn der Installer selbst aus dem Zielordner läuft, kann er # Programmdateien. Wenn der Installer selbst aus dem Zielordner laeuft, kann er
# sich nicht zuverlässig selbst löschen. Dann bleiben Setup + Ordner bis nach Ende stehen. # sich nicht zuverlaessig selbst loeschen. Dann bleiben Setup + Ordner bis nach Ende stehen.
$currentInstaller = [IO.Path]::GetFullPath($PSCommandPath) $currentInstaller = [IO.Path]::GetFullPath($PSCommandPath)
$targetFull = [IO.Path]::GetFullPath($InstallPath) $targetFull = [IO.Path]::GetFullPath($InstallPath)
@@ -2035,14 +2034,14 @@ function Uninstall-Relay {
Write-Ok "Lokale Programmdateien entfernt." Write-Ok "Lokale Programmdateien entfernt."
if ($currentInstaller.StartsWith($targetFull, [StringComparison]::OrdinalIgnoreCase)) { if ($currentInstaller.StartsWith($targetFull, [StringComparison]::OrdinalIgnoreCase)) {
Write-Warn "Der aktuell laufende Installer bleibt übrig. Nach dem Beenden kann '$InstallPath' manuell gelöscht werden." Write-Warn "Der aktuell laufende Installer bleibt uebrig. Nach dem Beenden kann '$InstallPath' manuell geloescht werden."
} }
Write-Title "Deinstallation abgeschlossen" Write-Title "Deinstallation abgeschlossen"
} }
function Show-Status { function Show-Status {
Write-Title "SMTPGraphRelay - Status" Write-Title "SMTPGraphRelay - Status anzeigen"
$configPath = Join-Path $InstallPath$ConfigFileName $configPath = Join-Path $InstallPath$ConfigFileName
Write-Host "Installationspfad: $InstallPath" Write-Host "Installationspfad: $InstallPath"
@@ -2050,7 +2049,7 @@ function Show-Status {
if (Test-Path -LiteralPath $configPath) { if (Test-Path -LiteralPath $configPath) {
Write-Ok "config.json vorhanden." Write-Ok "config.json vorhanden."
try { try {
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath$InstallPath
Write-Host " Sender: $($config.Graph.SenderMailbox) (ForceSender: $($config.Graph.ForceSender))" Write-Host " Sender: $($config.Graph.SenderMailbox) (ForceSender: $($config.Graph.ForceSender))"
Write-Host " SMTP: $($config.Smtp.ListenAddress):$($config.Smtp.Port)" Write-Host " SMTP: $($config.Smtp.ListenAddress):$($config.Smtp.Port)"
Write-Host " Client: $($config.Graph.ClientId)" Write-Host " Client: $($config.Graph.ClientId)"
@@ -2085,7 +2084,7 @@ function Show-Menu {
Write-Title "SMTPGraphRelay - Bootstrap / Repair / Online Update" Write-Title "SMTPGraphRelay - Bootstrap / Repair / Online Update"
Write-Host "Installationspfad: $InstallPath" -ForegroundColor DarkGray Write-Host "Installationspfad: $InstallPath" -ForegroundColor DarkGray
$installedVersion = Get-InstalledVersion -TargetPath $InstallPath $installedVersion = Get-InstalledVersion -TargetPath$InstallPath
if ($installedVersion -and$installedVersion.Version) { if ($installedVersion -and$installedVersion.Version) {
Write-Host "Installiert: $($installedVersion.Version)" -ForegroundColor DarkGray Write-Host "Installiert: $($installedVersion.Version)" -ForegroundColor DarkGray
@@ -2096,9 +2095,9 @@ function Show-Menu {
Write-Host " [1] Neuinstallation aus Gitea" Write-Host " [1] Neuinstallation aus Gitea"
Write-Host " [2] Installation aus Gitea reparieren" Write-Host " [2] Installation aus Gitea reparieren"
Write-Host " [3] Nach Online-Updates suchen" Write-Host " [3] Nach Online-Updates suchen"
Write-Host " [4] Entra App & Berechtigungen prüfen" Write-Host " [4] Entra App & Berechtigungen pruefen"
Write-Host " [5] Zertifikat erneuern" Write-Host " [5] Zertifikat erneuern"
Write-Host " [6] Health Check ausführen" Write-Host " [6] Health Check ausfuehren"
Write-Host " [7] Deinstallieren" Write-Host " [7] Deinstallieren"
Write-Host " [8] Status anzeigen" Write-Host " [8] Status anzeigen"
Write-Host " [9] SMTP-AUTH verwalten" Write-Host " [9] SMTP-AUTH verwalten"
@@ -2128,7 +2127,7 @@ while ($true) {
"10" { Manage-FailedQueue } "10" { Manage-FailedQueue }
"11" { Invoke-FailureModeTests } "11" { Invoke-FailureModeTests }
"0" { break } "0" { break }
default { Write-Warn "Ungültige Auswahl." } default { Write-Warn "Ungueltige Auswahl." }
} }
} }
catch { catch {
@@ -2141,7 +2140,7 @@ while ($true) {
if ($choice -ne "0") { if ($choice -ne "0") {
Write-Host "" Write-Host ""
Read-Host "Enter drücken, um zum Menü zurückzukehren" Read-Host "Enter druecken, um zum Menue zurueckzukehren"
} }
if ($choice -eq "0") { if ($choice -eq "0") {