From f3c7ec14d1ec8501c88765790ca4b79785c218ee Mon Sep 17 00:00:00 2001 From: "manuel.maier" Date: Tue, 13 Oct 2026 14:38:39 +0200 Subject: [PATCH] =?UTF-8?q?Setup-SMTPGraphRelay.ps1=20gel=C3=B6scht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Setup-SMTPGraphRelay.ps1 | 1160 -------------------------------------- 1 file changed, 1160 deletions(-) delete mode 100644 Setup-SMTPGraphRelay.ps1 diff --git a/Setup-SMTPGraphRelay.ps1 b/Setup-SMTPGraphRelay.ps1 deleted file mode 100644 index cad4985..0000000 --- a/Setup-SMTPGraphRelay.ps1 +++ /dev/null @@ -1,1160 +0,0 @@ -[CmdletBinding()] -param( - [string]$InstallPath = "$env:ProgramFiles\SMTPGraphRelay" -) - -#Requires -Version 5.1 -#Requires -RunAsAdministrator -<# -.SYNOPSIS - SMTPGraphRelay Installer / Repair / Update - -.DESCRIPTION - Einheitliches Verwaltungswerkzeug fuer SMTPGraphRelay. - - Modi: - 1 - Neuinstallation - 2 - Installation reparieren - 3 - Relay aktualisieren - 4 - Entra App & Berechtigungen pruefen - 5 - Zertifikat erneuern - 6 - Health Check ausfuehren - 7 - Deinstallieren - - WICHTIG: - Dieses Skript muss mit Windows PowerShell 5.1 ausgefuehrt werden. -#> - -$ErrorActionPreference = "Stop" -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -$TaskName = "SMTPGraphRelay" -$AppDefaultName = "SMTPGraphRelay" -$RelayFileName = "SMTPGraphRelay.ps1" -$HealthFileName = "Test-SMTPGraphRelay.ps1" -$RenewFileName = "Renew-SMTPGraphRelayCertificate.ps1" -$ConfigFileName = "config.json" - -# Zentrales Gitea-Repository / Updatequelle -$RepoBaseUrl = "https://me-gitea.maieredv.cloud/MAIEREDV/SMTPGraphRelay" -$RemoteVersionUrl = "$RepoBaseUrl/raw/branch/main/version.json" -$RemoteArchiveUrl = "$RepoBaseUrl/archive/main.zip" -$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", - "README.md" -) - -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 Read-Default { - param([string]$Prompt, [string]$Default) - $value = Read-Host "$Prompt [Standard: $Default]" - if ([string]::IsNullOrWhiteSpace($value)) { return $Default } - return $value -} - -function Confirm-Yes { - param([string]$Prompt) - $answer = Read-Host "$Prompt [j/N]" - return ($answer -match '^(?i)j|ja|y|yes$') -} - -function Assert-WindowsPowerShell51 { - if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) { - Write-Title "FALSCHE POWERSHELL-VERSION" - Write-Fail "Dieses Tool muss mit Windows PowerShell 5.1 ausgefuehrt werden." - exit 1 - } - Write-Ok "Windows PowerShell $($PSVersionTable.PSVersion) erkannt." -} - -function Ensure-PackageProvider { - try { - if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { - Write-Info "NuGet Package Provider wird installiert..." - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null - } - } catch { - Write-Warn "NuGet Provider konnte nicht automatisch vorbereitet werden: $($_.Exception.Message)" - } -} - -function Ensure-Modules { - Ensure-PackageProvider - - $modules = @( - "Microsoft.Graph.Authentication", - "Microsoft.Graph.Applications" - ) - - $allUsersPath = Join-Path ($env:ProgramFiles) "WindowsPowerShell\Modules" - - foreach ($module in $modules) { - $existing = Get-Module -ListAvailable -Name ($module) | - Where-Object { $_.ModuleBase -like "$allUsersPath*" } | - Sort-Object Version -Descending | - Select-Object -First 1 - - if (-not $existing) { - Write-Info "$module fehlt (System-weit). Installation fuer AllUsers..." - Install-Module ($module) -Scope AllUsers -Repository PSGallery -Force -AllowClobber - - $existing = Get-Module -ListAvailable -Name ($module) | - Where-Object { $_.ModuleBase -like "$allUsersPath*" } | - Sort-Object Version -Descending | - Select-Object -First 1 - } - - if (-not $existing) { - throw "Modul '$module' konnte nicht fuer AllUsers installiert/gefunden werden." - } - Write-Ok "$module verfuegbar (System-weit)." - } -} - -function New-RepoStagingArea { - Enable-Tls12 - - $root = Join-Path ($env:TEMP) ("SMTPGraphRelay-repo-{0}" -f [guid]::NewGuid().ToString("N")) - $archive = Join-Path ($root) "main.zip" - $extract = Join-Path ($root) "extract" - - New-Item -ItemType Directory -Path ($extract) -Force | Out-Null - - Write-Info "Lade aktuellen Stand aus Gitea..." - Invoke-WebRequest -Uri ($RemoteArchiveUrl) -OutFile ($archive) -UseBasicParsing -TimeoutSec 120 -ErrorAction Stop - - if (-not (Test-Path -LiteralPath ($archive))) { - throw "Gitea-Archiv wurde nicht heruntergeladen." - } - - Expand-Archive -LiteralPath ($archive) -DestinationPath ($extract) -Force - $releaseRoot = Find-ExtractedReleaseRoot -ExtractPath ($extract) - - foreach ($required in @("SMTPGraphRelay.ps1", "version.json")) { - if (-not (Test-Path -LiteralPath (Join-Path ($releaseRoot) $required))) { - throw "Repository-Archiv ist unvollstaendig: '$required' fehlt." - } - } - - $versionInfo = Get-Content -LiteralPath (Join-Path ($releaseRoot) "version.json") -Raw -Encoding UTF8 | ConvertFrom-Json - - if (-not $versionInfo.Version) { - throw "version.json aus dem Repository enthaelt keine Version." - } - - Write-Ok "Repository-Version $($versionInfo.Version) geladen." - - return [pscustomobject]@{ - TempRoot = $root - ReleaseRoot = $releaseRoot - VersionInfo = $versionInfo - } -} - -function Remove-RepoStagingArea { - param($Staging) - if ($Staging -and $Staging.TempRoot) { - Remove-Item -LiteralPath ($Staging.TempRoot) -Recurse -Force -ErrorAction SilentlyContinue - } -} - -function Install-RepoProgramFiles { - param( - [Parameter(Mandatory)][string]$ReleaseRoot, - [Parameter(Mandatory)][string]$TargetPath - ) - - New-Item -ItemType Directory -Path ($TargetPath) -Force | Out-Null - - foreach ($name in $ManagedReleaseFiles) { - $source = Join-Path ($ReleaseRoot) $name - if (Test-Path -LiteralPath ($source)) { - Copy-Item -LiteralPath ($source) -Destination (Join-Path ($TargetPath) $name) -Force - Write-Ok "Installiert: $name" - } - } -} - -function Get-SourceFile { - param([Parameter(Mandatory)][string]$Name) - $candidate = Join-Path ($PSScriptRoot) $Name - if (Test-Path -LiteralPath ($candidate)) { - return $candidate - } - return $null -} - -function Get-InstalledVersion { - param([Parameter(Mandatory)][string]$TargetPath) - $versionFile = Join-Path ($TargetPath) "version.json" - if (-not (Test-Path -LiteralPath ($versionFile))) { - return $null - } - try { - return Get-Content -LiteralPath ($versionFile) -Raw -Encoding UTF8 | ConvertFrom-Json - } - catch { - return $null - } -} - -function Ensure-Directories { - param([Parameter(Mandatory)][string]$TargetPath) - - foreach ($dir in @( - $TargetPath, - (Join-Path ($TargetPath) "queue"), - (Join-Path ($TargetPath) "queue\incoming"), - (Join-Path ($TargetPath) "queue\pending"), - (Join-Path ($TargetPath) "queue\processing"), - (Join-Path ($TargetPath) "failed"), - (Join-Path ($TargetPath) "logs") - )) { - New-Item -ItemType Directory -Path ($dir) -Force | Out-Null - } - Write-Ok "Programm-/Queue-/Log-Verzeichnisse vorhanden." -} - -function Get-RelayConfig { - param([Parameter(Mandatory)][string]$TargetPath) - - $path = Join-Path ($TargetPath) $ConfigFileName - if (-not (Test-Path -LiteralPath ($path))) { - return $null - } - try { - return Get-Content -LiteralPath ($path) -Raw -Encoding UTF8 | ConvertFrom-Json - } - catch { - throw "config.json konnte nicht gelesen werden: $($_.Exception.Message)" - } -} - -function Write-RelayConfig { - param( - [Parameter(Mandatory)]$Config, - [Parameter(Mandatory)][string]$TargetPath - ) - - $configPath = Join-Path ($TargetPath) $ConfigFileName - $tmp = "$configPath.tmp" - - $Config | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath ($tmp) -Encoding UTF8 - Move-Item -LiteralPath ($tmp) -Destination ($configPath) -Force - Write-Ok "config.json geschrieben." -} - -function Ensure-FirewallRule { - param([Parameter(Mandatory)][int]$Port) - - $prefix = "SMTPGraphRelay TCP " - Get-NetFirewallRule -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -like "$prefix*" -and $_.DisplayName -ne "$prefix$Port" } | - Remove-NetFirewallRule -ErrorAction SilentlyContinue - - $ruleName = "$prefix$Port" - $existing = Get-NetFirewallRule -DisplayName ($ruleName) -ErrorAction SilentlyContinue - - if (-not $existing) { - New-NetFirewallRule -DisplayName ($ruleName) -Direction Inbound -Action Allow -Protocol TCP -LocalPort ($Port) -Profile Any | Out-Null - Write-Ok "Firewallregel '$ruleName' erstellt." - } - else { - Write-Ok "Firewallregel '$ruleName' vorhanden." - } -} - -function Ensure-ScheduledTask { - param([Parameter(Mandatory)][string]$TargetPath) - - $scriptPath = Join-Path ($TargetPath) $RelayFileName - if (-not (Test-Path -LiteralPath ($scriptPath))) { - throw "Relay-Skript fehlt: $scriptPath" - } - - $psExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" - $configPath = Join-Path ($TargetPath) $ConfigFileName - - $action = New-ScheduledTaskAction -Execute ($psExe) -Argument "-NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -ConfigPath `"$configPath`"" -WorkingDirectory ($TargetPath) - $trigger = New-ScheduledTaskTrigger -AtStartup - $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) - - Register-ScheduledTask -TaskName ($TaskName) -Action ($action) -Trigger ($trigger) -Principal ($principal) -Settings ($settings) -Description "Lokaler SMTP Store-and-Forward Relay zu Microsoft 365" -Force | Out-Null - Write-Ok "Scheduled Task '$TaskName' eingerichtet." -} - -function Stop-RelayTask { - $task = Get-ScheduledTask -TaskName ($TaskName) -ErrorAction SilentlyContinue - if (-not $task -or $task.State -ne "Running") { return } - - $graceSeconds = 30 - try { - $config = Get-RelayConfig -TargetPath ($InstallPath) - if ($config -and $config.Smtp.PSObject.Properties.Name -contains "GracefulShutdownSeconds") { - $configured = [int]$config.Smtp.GracefulShutdownSeconds - if ($configured -ge 5 -and $configured -le 300) { $graceSeconds = $configured } - } - } catch {} - - $signalPath = Join-Path ($InstallPath) "shutdown.request" - - try { - [IO.File]::WriteAllText($signalPath, ([DateTime]::UtcNow.ToString("o")), (New-Object Text.UTF8Encoding($false))) - Write-Info "Graceful Shutdown angefordert. Warte auf Relay (max. $graceSeconds Sekunden)..." - $deadline = (Get-Date).AddSeconds($graceSeconds + 5) - - while ((Get-Date) -lt $deadline) { - Start-Sleep -Milliseconds 250 - $current = Get-ScheduledTask -TaskName ($TaskName) -ErrorAction SilentlyContinue - if (-not $current -or $current.State -ne "Running") { - Write-Ok "Relay sauber beendet." - return - } - } - Write-Warn "Graceful-Shutdown-Timeout erreicht. Task wird hart beendet." - Stop-ScheduledTask -TaskName ($TaskName) -ErrorAction SilentlyContinue - Start-Sleep -Milliseconds 750 - } - finally { - Remove-Item -LiteralPath ($signalPath) -Force -ErrorAction SilentlyContinue - } -} - -function Start-RelayTask { - $task = Get-ScheduledTask -TaskName ($TaskName) -ErrorAction SilentlyContinue - if ($task) { - Remove-Item -LiteralPath (Join-Path ($InstallPath) "shutdown.request") -Force -ErrorAction SilentlyContinue - Start-ScheduledTask -TaskName ($TaskName) - Start-Sleep -Seconds 2 - Write-Ok "Scheduled Task gestartet." - } -} - -function Convert-CertToKeyCredential { - param([Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) - return @{ - Type = "AsymmetricX509Cert" - Usage = "Verify" - Key = $Certificate.GetRawCertData() - DisplayName = "SMTPGraphRelay Certificate" - StartDateTime = $Certificate.NotBefore.ToUniversalTime() - EndDateTime = $Certificate.NotAfter.ToUniversalTime() - } -} - -function New-RelayCertificate { - $subject = "CN=SMTPGraphRelay-$env:COMPUTERNAME" - return New-SelfSignedCertificate -Subject ($subject) -CertStoreLocation "Cert:\LocalMachine\My" -KeyAlgorithm RSA -KeyLength 2048 -HashAlgorithm SHA256 -KeyExportPolicy NonExportable -KeySpec Signature -NotAfter (Get-Date).AddYears(2) -} - -function Connect-RelayGraphAdmin { - param([string]$TenantId) - Import-Module Microsoft.Graph.Authentication -Force -ErrorAction Stop - Import-Module Microsoft.Graph.Applications -Force -ErrorAction Stop - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - - $scopes = @("Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All") - if ([string]::IsNullOrWhiteSpace($TenantId)) { - Connect-MgGraph -Scopes ($scopes) -NoWelcome - } else { - Connect-MgGraph -TenantId ($TenantId) -Scopes ($scopes) -NoWelcome - } -} - -function Enable-Tls12 { - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - } catch {} -} - -function Get-RemoteVersion { - Enable-Tls12 - $tempFile = Join-Path ($env:TEMP) ("SMTPGraphRelay-version-{0}.json" -f [guid]::NewGuid().ToString("N")) - try { - Invoke-WebRequest -Uri ($RemoteVersionUrl) -OutFile ($tempFile) -UseBasicParsing -TimeoutSec 20 -ErrorAction Stop - $remote = Get-Content -LiteralPath ($tempFile) -Raw -Encoding UTF8 | ConvertFrom-Json - if (-not $remote.Version) { throw "Remote version.json enthaelt keine Version." } - return $remote - } - finally { - Remove-Item -LiteralPath ($tempFile) -Force -ErrorAction SilentlyContinue - } -} - -function Compare-RelayVersions { - param([Parameter(Mandatory)][string]$Installed, [Parameter(Mandatory)][string]$Remote) - try { - $installedVersion = [version]$Installed - $remoteVersion = [version]$Remote - if ($remoteVersion -gt $installedVersion) { return 1 } - if ($remoteVersion -lt $installedVersion) { return -1 } - return 0 - } - catch { - throw "Versionsvergleich fehlgeschlagen: installiert='$Installed', remote='$Remote'." - } -} - -function Find-ExtractedReleaseRoot { - param([Parameter(Mandatory)][string]$ExtractPath) - $relayFiles = Get-ChildItem -LiteralPath ($ExtractPath) -Recurse -File -Filter ($RelayFileName) -ErrorAction SilentlyContinue - foreach ($relay in $relayFiles) { - $candidate = $relay.Directory.FullName - if (Test-Path -LiteralPath (Join-Path ($candidate) "version.json")) { - return $candidate - } - } - throw "Im heruntergeladenen Archiv wurde kein gueltiges SMTPGraphRelay-Release gefunden." -} - -function Backup-ManagedFiles { - param([Parameter(Mandatory)][string]$TargetPath) - $backupRoot = Join-Path ($TargetPath) "backup" - $backupPath = Join-Path ($backupRoot) (Get-Date -Format "yyyyMMdd-HHmmss") - New-Item -ItemType Directory -Path ($backupPath) -Force | Out-Null - foreach ($name in $ManagedReleaseFiles) { - $source = Join-Path ($TargetPath) $name - if (Test-Path -LiteralPath ($source)) { - Copy-Item -LiteralPath ($source) -Destination (Join-Path ($backupPath) $name) -Force - } - } - return $backupPath -} - -function Restore-ManagedFiles { - param([Parameter(Mandatory)][string]$BackupPath, [Parameter(Mandatory)][string]$TargetPath) - foreach ($name in $ManagedReleaseFiles) { - $backupFile = Join-Path ($BackupPath) $name - $targetFile = Join-Path ($TargetPath) $name - if (Test-Path -LiteralPath ($backupFile)) { - Copy-Item -LiteralPath ($backupFile) -Destination ($targetFile) -Force - } - } -} - -function Install-ExtractedRelease { - param([Parameter(Mandatory)][string]$ReleaseRoot, [Parameter(Mandatory)][string]$TargetPath) - $required = @("SMTPGraphRelay.ps1", "version.json") - foreach ($name in $required) { - if (-not (Test-Path -LiteralPath (Join-Path ($ReleaseRoot) $name))) { - throw "Updatepaket ist unvollstaendig: '$name' fehlt." - } - } - foreach ($name in $ManagedReleaseFiles) { - $source = Join-Path ($ReleaseRoot) $name - if (Test-Path -LiteralPath ($source)) { - Copy-Item -LiteralPath ($source) -Destination (Join-Path ($TargetPath) $name) -Force - Write-Ok "Aktualisiert: $name" - } - } -} - -function Invoke-PostUpdateHealthCheck { - param([Parameter(Mandatory)][string]$TargetPath) - $health = Join-Path ($TargetPath) $HealthFileName - if (-not (Test-Path -LiteralPath ($health))) { - Write-Warn "Health Check ist nicht installiert; automatische Nachpruefung entfaellt." - return 0 - } - Write-Info "Starte Health Check nach dem Update..." - & $health -ConfigPath (Join-Path ($TargetPath) $ConfigFileName) - return $LASTEXITCODE -} - -function Install-New { - Write-Title "SMTPGraphRelay - Neuinstallation aus Gitea" - - if (Test-Path -LiteralPath (Join-Path ($InstallPath) $ConfigFileName)) { - Write-Warn "Es existiert bereits eine config.json." - if (-not (Confirm-Yes "Trotzdem fortfahren?")) { return } - } - - $staging = $null - try { - $staging = New-RepoStagingArea - Ensure-Modules - Ensure-Directories -TargetPath ($InstallPath) - Install-RepoProgramFiles -ReleaseRoot ($staging.ReleaseRoot) -TargetPath ($InstallPath) - Write-Ok "Programmdateien aus Gitea installiert." - - $appName = Read-Default "Name der Entra App" $AppDefaultName - $senderMailbox = Read-Host "Standard-Absenderpostfach (Fallback/Test, z.B. info@firma.de)" - while ([string]::IsNullOrWhiteSpace($senderMailbox) -or $senderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') { - $senderMailbox = Read-Host "Bitte eine gueltige Mailadresse eingeben" - } - - $listenAddress = Read-Default "Lokale Listen-IP" "0.0.0.0" - $port = [int](Read-Default "SMTP-Port" "2525") - $allowedText = Read-Default "Erlaubte Netze, mit Komma getrennt" "127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" - $allowedNetworks = @($allowedText -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) - - Write-Info "Erzeuge Relay-Zertifikat..." - $cert = New-RelayCertificate - Write-Ok "Zertifikat erstellt: $($cert.Thumbprint)" - - Write-Info "Microsoft Graph Anmeldung erforderlich (Entra-Admin)." - Connect-RelayGraphAdmin - - try { - $tenantId = (Get-MgContext).TenantId - Write-Ok "Tenant: $tenantId" - - $appParams = @{ - DisplayName = $appName - SignInAudience = "AzureADMyOrg" - KeyCredentials = @((Convert-CertToKeyCredential -Certificate ($cert))) - } - - $app = New-MgApplication -BodyParameter ($appParams) - Write-Ok "App Registration erstellt: $($app.AppId)" - - $sp = $null - for ($i = 0; $i -lt 10 -and -not $sp; $i++) { - try { $sp = New-MgServicePrincipal -AppId ($app.AppId) } catch { Start-Sleep -Seconds 2 } - } - - if (-not $sp) { throw "Entra Service Principal konnte nicht erstellt werden." } - Write-Ok "Entra Service Principal erstellt: $($sp.Id)" - - Write-Info "Erteile globale 'Mail.Send' Application Berechtigung (Tenant-weiter Versand)..." - $graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" -Property "id,appRoles" - $mailSendRole = $graphSp.AppRoles | Where-Object { $_.Value -eq "Mail.Send" -and $_.AllowedMemberTypes -contains "Application" } | Select-Object -First 1 - - if (-not $mailSendRole) { throw "Microsoft Graph AppRole 'Mail.Send' konnte nicht gefunden werden." } - - $existingAssig = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId ($sp.Id) -All -ErrorAction SilentlyContinue | Where-Object { $_.AppRoleId -eq $mailSendRole.Id } - - if (-not $existingAssig) { - New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId ($sp.Id) -PrincipalId ($sp.Id) -ResourceId ($graphSp.Id) -AppRoleId ($mailSendRole.Id) | Out-Null - Write-Ok "Globale 'Mail.Send' Application Role erfolgreich vergeben." - } else { - Write-Ok "Globale 'Mail.Send' Berechtigung war bereits vergeben." - } - - $config = [ordered]@{ - Smtp = [ordered]@{ - ListenAddress = $listenAddress - Port = $port - Hostname = $env:COMPUTERNAME - AllowedNetworks = $allowedNetworks - MaxMessageSizeMB = 25 - ClientTimeoutSeconds = 120 - MaxConcurrentClients = 20 - MaxRecipients = 50 - MaxMessagesPerConnection = 25 - RequireAuth = $false - AuthMaxFailures = 5 - AllowUnauthenticatedNetworks = @() - AuthUsers = @() - GracefulShutdownSeconds = 30 - } - Graph = [ordered]@{ - TenantId = $tenantId - ClientId = $app.AppId - CertificateThumbprint = $cert.Thumbprint - SenderMailbox = $senderMailbox - ForceSender = $false - CertificateWarningDays = 60 - CertificateCriticalDays = 14 - CertificateCheckHours = 12 - } - Queue = [ordered]@{ - PollSeconds = 10 - MaxRetries = 8 - RetryMinutes = @(1,5,15,30,60,120,240,480) - MaxPendingMessages = 5000 - MinFreeDiskSpaceMB = 1024 - } - Paths = [ordered]@{ - Queue = "queue" - Failed = "failed" - Logs = "logs" - } - Logging = [ordered]@{ - MaxFileSizeMB = 10 - RetentionDays = 30 - CleanupHours = 12 - } - Update = [ordered]@{ - Repository = $RepoBaseUrl - Branch = "main" - } - } - - Write-RelayConfig -Config ($config) -TargetPath ($InstallPath) - Ensure-FirewallRule -Port ($port) - Ensure-ScheduledTask -TargetPath ($InstallPath) - - Write-Info "Teste App-only Anmeldung mit Relay-Zertifikat..." - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - Connect-MgGraph -TenantId ($tenantId) -ClientId ($app.AppId) -Certificate ($cert) -NoWelcome | Out-Null - - $ctx = Get-MgContext - if (-not $ctx -or $ctx.AuthType -ne "AppOnly") { throw "App-only Anmeldung konnte nicht bestaetigt werden." } - Write-Ok "App-only Anmeldung funktioniert." - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - - Start-RelayTask - - $health = Join-Path ($InstallPath) $HealthFileName - if (Test-Path -LiteralPath ($health)) { - Write-Info "Starte abschliessenden Health Check..." - & $health -ConfigPath (Join-Path ($InstallPath) $ConfigFileName) - if ($LASTEXITCODE -ge 2) { Write-Warn "Health Check meldet Fehler. Bitte Ausgabe pruefen." } - } - - Write-Title "Neuinstallation abgeschlossen" - Write-Host "Installationspfad: $InstallPath" - } - finally { - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - } - } - finally { - Remove-RepoStagingArea -Staging ($staging) - } -} - -function Repair-Installation { - Write-Title "SMTPGraphRelay - Repair aus Gitea" - $config = Get-RelayConfig -TargetPath ($InstallPath) - if (-not $config) { - Write-Fail "Keine config.json gefunden. Repair ist nur fuer bestehende Installationen gedacht." - return - } - - $staging = $null - $backupPath = $null - try { - $staging = New-RepoStagingArea - Ensure-Modules - Ensure-Directories -TargetPath ($InstallPath) - - $certPath = "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)" - $cert = Get-Item -LiteralPath ($certPath) -ErrorAction SilentlyContinue - if (-not $cert) { Write-Fail "Konfiguriertes Zertifikat fehlt." } - elseif (-not $cert.HasPrivateKey) { Write-Fail "Konfiguriertes Zertifikat besitzt keinen privaten Schluessel." } - else { Write-Ok "Zertifikat vorhanden." } - - $backupPath = Backup-ManagedFiles -TargetPath ($InstallPath) - Stop-RelayTask - Install-RepoProgramFiles -ReleaseRoot ($staging.ReleaseRoot) -TargetPath ($InstallPath) - Ensure-FirewallRule -Port ([int]$config.Smtp.Port) - Ensure-ScheduledTask -TargetPath ($InstallPath) - Start-RelayTask - - $healthExit = Invoke-PostUpdateHealthCheck -TargetPath ($InstallPath) - if ($healthExit -ge 2) { throw "Health Check nach Repair meldet FEHLER." } - Write-Title "Repair abgeschlossen" - } - catch { - Write-Fail "Repair fehlgeschlagen: $($_.Exception.Message)" - if ($backupPath -and (Test-Path -LiteralPath ($backupPath))) { - try { - Stop-RelayTask - Restore-ManagedFiles -BackupPath ($backupPath) -TargetPath ($InstallPath) - Ensure-ScheduledTask -TargetPath ($InstallPath) - Start-RelayTask - } catch {} - } - } - finally { - Remove-RepoStagingArea -Staging ($staging) - } -} - -function Update-Relay { - Write-Title "SMTPGraphRelay - Online Update" - $config = Get-RelayConfig -TargetPath ($InstallPath) - if (-not $config) { - Write-Fail "Keine bestehende config.json gefunden." - return - } - - $installedVersionInfo = Get-InstalledVersion -TargetPath ($InstallPath) - $installedVersion = if ($installedVersionInfo) { [string]$installedVersionInfo.Version } else { Read-Host "Installierte Version manuell eingeben" } - if ([string]::IsNullOrWhiteSpace($installedVersion)) { return } - - try { $remoteInfo = Get-RemoteVersion } catch { Write-Fail "Remote-Version konnte nicht geladen werden."; return } - $remoteVersion = [string]$remoteInfo.Version - - $comparison = Compare-RelayVersions -Installed ($installedVersion) -Remote ($remoteVersion) - if ($comparison -eq 0) { Write-Ok "Bereits aktuell ($installedVersion)."; return } - if (-not (Confirm-Yes "Update von $installedVersion auf $remoteVersion durchfuehren?")) { return } - - $updateRoot = Join-Path ($env:TEMP) ("SMTPGraphRelay-update-{0}" -f [guid]::NewGuid().ToString("N")) - $archivePath = Join-Path ($updateRoot) "main.zip" - $extractPath = Join-Path ($updateRoot) "extract" - New-Item -ItemType Directory -Path ($updateRoot) -Force | Out-Null - New-Item -ItemType Directory -Path ($extractPath) -Force | Out-Null - - $backupPath = $null - try { - Invoke-WebRequest -Uri ($RemoteArchiveUrl) -OutFile ($archivePath) -UseBasicParsing -TimeoutSec 120 -ErrorAction Stop - Expand-Archive -LiteralPath ($archivePath) -DestinationPath ($extractPath) -Force - $releaseRoot = Find-ExtractedReleaseRoot -ExtractPath ($extractPath) - - $backupPath = Backup-ManagedFiles -TargetPath ($InstallPath) - Stop-RelayTask - Install-ExtractedRelease -ReleaseRoot ($releaseRoot) -TargetPath ($InstallPath) - Ensure-Directories -TargetPath ($InstallPath) - Ensure-FirewallRule -Port ([int]$config.Smtp.Port) - Ensure-ScheduledTask -TargetPath ($InstallPath) - Start-RelayTask - - $healthExit = Invoke-PostUpdateHealthCheck -TargetPath ($InstallPath) - if ($healthExit -ge 2) { throw "Health Check nach Update meldet FEHLER." } - Write-Title "Online Update abgeschlossen" - } - catch { - Write-Fail "Update fehlgeschlagen: $($_.Exception.Message)" - if ($backupPath -and (Test-Path -LiteralPath ($backupPath))) { - try { - Stop-RelayTask - Restore-ManagedFiles -BackupPath ($backupPath) -TargetPath ($InstallPath) - Ensure-ScheduledTask -TargetPath ($InstallPath) - Start-RelayTask - } catch {} - } - } - finally { - Remove-Item -LiteralPath ($updateRoot) -Recurse -Force -ErrorAction SilentlyContinue - } -} - -function Verify-CloudPermissions { - Write-Title "SMTPGraphRelay - Entra Berechtigungen pruefen" - $config = Get-RelayConfig -TargetPath ($InstallPath) - if (-not $config) { return } - - Ensure-Modules - Connect-RelayGraphAdmin -TenantId ($config.Graph.TenantId) - - try { - $app = Get-MgApplication -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id,appId,displayName" | Select-Object -First 1 - if (-not $app) { Write-Fail "App Registration nicht gefunden."; return } - - $sp = Get-MgServicePrincipal -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id,appId,displayName" | Select-Object -First 1 - if (-not $sp) { Write-Fail "Entra Service Principal nicht gefunden."; return } - - $graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" -Property "id,appRoles" - $mailSendRole = $graphSp.AppRoles | Where-Object { $_.Value -eq "Mail.Send" -and $_.AllowedMemberTypes -contains "Application" } | Select-Object -First 1 - - $globalAssig = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId ($sp.Id) -All -ErrorAction SilentlyContinue | Where-Object { $_.AppRoleId -eq $mailSendRole.Id } - - if ($globalAssig) { Write-Ok "GLOBALE (tenant-weite) Mail.Send Berechtigung vorhanden." } - else { Write-Fail "GLOBALE Mail.Send Berechtigung fehlt!" } - } - finally { - Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null - } -} - -function Invoke-CertificateRenewal { - $renew = Join-Path ($InstallPath) $RenewFileName - if (-not (Test-Path -LiteralPath ($renew))) { return } - & $renew -ConfigPath (Join-Path ($InstallPath) $ConfigFileName) -} - -function Invoke-HealthCheck { - $health = Join-Path ($InstallPath) $HealthFileName - if (-not (Test-Path -LiteralPath ($health))) { return } - & $health -ConfigPath (Join-Path ($InstallPath) $ConfigFileName) -} - -function Invoke-Pbkdf2Sha256 { - param([Parameter(Mandatory)][string]$Password, [Parameter(Mandatory)][byte[]]$Salt, [Parameter(Mandatory)][int]$Iterations, [int]$Length = 32) - try { - $derive = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Password, $Salt, $Iterations, [System.Security.Cryptography.HashAlgorithmName]::SHA256) - try { return $derive.GetBytes($Length) } finally { $derive.Dispose() } - } - catch { - $hmac = New-Object System.Security.Cryptography.HMACSHA256 - $hmac.Key = [Text.Encoding]::UTF8.GetBytes($Password) - try { - $hashLength = 32 - $blocks = [Math]::Ceiling($Length / [double]$hashLength) - $output = New-Object byte[] ($blocks * $hashLength) - $offset = 0 - - for ($block = 1; $block -le $blocks; $block++) { - $blockBytes = [BitConverter]::GetBytes([int]$block) - if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($blockBytes) } - $input = New-Object byte[] ($Salt.Length + 4) - [Array]::Copy($Salt, 0, $input, 0, $Salt.Length) - [Array]::Copy($blockBytes, 0, $input, $Salt.Length, 4) - $u = $hmac.ComputeHash($input) - $t = New-Object byte[] $u.Length - [Array]::Copy($u, $t, $u.Length) - for ($i = 2; $i -le $Iterations; $i++) { - $u = $hmac.ComputeHash($u) - for ($j = 0; $j -lt $t.Length; $j++) { $t[$j] = $t[$j] -bxor $u[$j] } - } - [Array]::Copy($t, 0, $output, $offset, $t.Length) - $offset += $t.Length - } - $result = New-Object byte[] $Length - [Array]::Copy($output, 0, $result, 0, $Length) - return $result - } finally { $hmac.Dispose() } - } -} - -function New-SmtpPasswordRecord { - param([Parameter(Mandatory)][string]$Password) - $salt = New-Object byte[] 16 - $rng = [Security.Cryptography.RandomNumberGenerator]::Create() - try { $rng.GetBytes($salt) } finally { $rng.Dispose() } - - $iterations = 150000 - $hash = Invoke-Pbkdf2Sha256 -Password ($Password) -Salt ($salt) -Iterations ($iterations) -Length 32 - return [pscustomobject]@{ Salt = [Convert]::ToBase64String($salt); PasswordHash = [Convert]::ToBase64String($hash); Iterations = $iterations } -} - -function Ensure-SmtpAuthConfig { - param([Parameter(Mandatory)]$Config) - if (-not ($Config.Smtp.PSObject.Properties.Name -contains "RequireAuth")) { $Config.Smtp | Add-Member -NotePropertyName RequireAuth -NotePropertyValue $false } - if (-not ($Config.Smtp.PSObject.Properties.Name -contains "AuthMaxFailures")) { $Config.Smtp | Add-Member -NotePropertyName AuthMaxFailures -NotePropertyValue 5 } - if (-not ($Config.Smtp.PSObject.Properties.Name -contains "AllowUnauthenticatedNetworks")) { $Config.Smtp | Add-Member -NotePropertyName AllowUnauthenticatedNetworks -NotePropertyValue @() } - if (-not ($Config.Smtp.PSObject.Properties.Name -contains "AuthUsers")) { $Config.Smtp | Add-Member -NotePropertyName AuthUsers -NotePropertyValue @() } - return $Config -} - -function Save-SmtpAuthConfigAndRestart { - param([Parameter(Mandatory)]$Config) - Write-RelayConfig -Config ($Config) -TargetPath ($InstallPath) - Ensure-ScheduledTask -TargetPath ($InstallPath) - Stop-RelayTask - Start-RelayTask -} - -function Get-FailedQueuePath { - param([Parameter(Mandatory)]$Config) - $path = [string]$Config.Paths.Failed - if ([IO.Path]::IsPathRooted($path)) { return $path } - return (Join-Path ($InstallPath) $path) -} - -function Get-PendingQueuePath { - param([Parameter(Mandatory)]$Config) - $path = [string]$Config.Paths.Queue - if (-not [IO.Path]::IsPathRooted($path)) { $path = Join-Path ($InstallPath) $path } - return (Join-Path ($path) "pending") -} - -function Get-FailedQueueEntries { - param([Parameter(Mandatory)]$Config) - $failedPath = Get-FailedQueuePath -Config ($Config) - if (-not (Test-Path -LiteralPath ($failedPath))) { return @() } - $entries = @() - foreach ($file in Get-ChildItem -LiteralPath ($failedPath) -Filter "*.eml" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime) { - $metaPath = "$($file.FullName).json" - $meta = $null - if (Test-Path -LiteralPath ($metaPath)) { try { $meta = Get-Content -LiteralPath ($metaPath) -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} } - - $queueId = [IO.Path]::GetFileNameWithoutExtension($file.Name) - if ($meta -and $meta.PSObject.Properties.Name -contains "QueueId" -and $meta.QueueId) { $queueId = [string]$meta.QueueId } - - $entries += [pscustomobject]@{ - QueueId = $queueId - FileName = $file.Name - Path = $file.FullName - 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 } - From = if ($meta) { [string]$meta.EnvelopeFrom } else { "" } - Recipients = if ($meta) { (@($meta.EnvelopeRecipients) -join ", ") } else { "" } - LastStatusCode = if ($meta) { [string]$meta.LastStatusCode } else { "" } - LastError = if ($meta) { [string]$meta.LastError } else { "" } - AuthenticatedUser = if ($meta -and $meta.PSObject.Properties.Name -contains "AuthenticatedUser") { [string]$meta.AuthenticatedUser } else { "" } - Meta = $meta - } - } - return @($entries) -} - -function Retry-FailedQueueEntry { - param([Parameter(Mandatory)]$Config, [Parameter(Mandatory)]$Entry) - $pendingPath = Get-PendingQueuePath -Config ($Config) - New-Item -ItemType Directory -Path ($pendingPath) -Force | Out-Null - $targetEml = Join-Path ($pendingPath) $Entry.FileName - $targetMeta = "$targetEml.json" - - if (Test-Path -LiteralPath ($targetEml)) { throw "Pending enthaelt bereits '$($Entry.FileName)'." } - - if ($Entry.Meta) { - $meta = $Entry.Meta - if ($meta.PSObject.Properties.Name -contains "RetryCount") { $meta.RetryCount = 0 } else { $meta | Add-Member -NotePropertyName RetryCount -NotePropertyValue 0 } - $now = [DateTime]::UtcNow.ToString("o") - if ($meta.PSObject.Properties.Name -contains "NextAttemptUtc") { $meta.NextAttemptUtc = $now } else { $meta | Add-Member -NotePropertyName NextAttemptUtc -NotePropertyValue $now } - if ($meta.PSObject.Properties.Name -contains "RequeuedUtc") { $meta.RequeuedUtc = $now } else { $meta | Add-Member -NotePropertyName RequeuedUtc -NotePropertyValue $now } - $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.Path) -Destination ($targetEml) -Force -} - -function Remove-FailedQueueEntry { - param([Parameter(Mandatory)]$Entry) - Remove-Item -LiteralPath ($Entry.Path) -Force -ErrorAction Stop - Remove-Item -LiteralPath ($Entry.MetaPath) -Force -ErrorAction SilentlyContinue -} - -function Manage-FailedQueue { - $config = Get-RelayConfig -TargetPath ($InstallPath) - if (-not $config) { Write-Fail "config.json nicht gefunden."; return } - - while ($true) { - Clear-Host - Write-Title "SMTPGraphRelay - Failed Queue" - $entries = @(Get-FailedQueueEntries -Config ($config)) - - Write-Host " [1] Failed Queue anzeigen" - Write-Host " [2] Details einer Mail anzeigen" - Write-Host " [3] Eine Mail erneut zustellen" - Write-Host " [4] Alle Mails erneut zustellen" - Write-Host " [5] Eine Mail endgueltig loeschen" - Write-Host " [6] Alle Failed-Mails endgueltig loeschen" - Write-Host " [0] Zurueck`n" - - $choice = Read-Host "Auswahl" - - switch ($choice) { - "1" { if ($entries.Count -eq 0) { Write-Ok "Failed Queue ist leer." } else { $entries | Select-Object QueueId,FailedSince,RetryCount,From,Recipients,LastStatusCode,SizeKB | Format-Table -AutoSize }; Read-Host "Enter" } - "2" { - $id = Read-Host "Queue-ID" - $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 { Write-Host "`nFehler: $($entry.LastError)" -ForegroundColor Yellow } - Read-Host "Enter" - } - "3" { - $id = Read-Host "Queue-ID" - $entry = $entries | Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } | Select-Object -First 1 - if ($entry) { Retry-FailedQueueEntry -Config ($config) -Entry ($entry); Write-Ok "[$($entry.QueueId)] zurueck nach pending verschoben." } - Read-Host "Enter" - } - "4" { - if (Confirm-Yes "Alle Mails erneut zustellen?") { - foreach ($entry in $entries) { try { Retry-FailedQueueEntry -Config ($config) -Entry ($entry) } catch {} } - } - } - "5" { - $id = Read-Host "Queue-ID" - $entry = $entries | Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } | Select-Object -First 1 - if ($entry -and (Confirm-Yes "Loeschen?")) { Remove-FailedQueueEntry -Entry ($entry); Write-Ok "Geloescht." } - Read-Host "Enter" - } - "6" { - if (Confirm-Yes "Alle loeschen?") { foreach ($entry in $entries) { Remove-FailedQueueEntry -Entry ($entry) } } - } - "0" { return } - } - $config = Get-RelayConfig -TargetPath ($InstallPath) - } -} - -function Manage-SmtpAuth { - $config = Get-RelayConfig -TargetPath ($InstallPath); - if (-not $config) { Write-Fail "config.json nicht gefunden."; return; } - $config = Ensure-SmtpAuthConfig -Config ($config); - - while ($true) { - Clear-Host; - Write-Title "SMTPGraphRelay - SMTP-AUTH"; - - $users = @($config.Smtp.AuthUsers); - $authState = if ([bool]$config.Smtp.RequireAuth) { "ERFORDERLICH" } else { "optional / nicht erforderlich" }; - - Write-Host "Status: $authState"; - Write-Host "Benutzer: $($users.Count)"; - Write-Host "Max. Fehlversuche: $($config.Smtp.AuthMaxFailures)"; - Write-Host "Ohne Auth erlaubte Netze: $(@($config.Smtp.AllowUnauthenticatedNetworks) -join ', ')"; - Write-Host ""; - Write-Host " [1] SMTP-AUTH erforderlich EIN/AUS"; - Write-Host " [2] Benutzer hinzufuegen"; - Write-Host " [3] Benutzer anzeigen"; - Write-Host " [4] Passwort aendern"; - Write-Host " [5] Benutzer loeschen"; - Write-Host " [6] Netze ohne Auth verwalten"; - Write-Host " [7] Max. Fehlversuche aendern"; - Write-Host " [0] Zurueck`n"; - - $choice = Read-Host "Auswahl"; - - switch ($choice) { - "1" { - $config.Smtp.RequireAuth = -not [bool]$config.Smtp.RequireAuth; - if ($config.Smtp.RequireAuth -and @($config.Smtp.AuthUsers).Count -eq 0) { - Write-Warn "AUTH wurde aktiviert, aber es existiert noch kein SMTP-Benutzer."; - } - Save-SmtpAuthConfigAndRestart -Config ($config); - Write-Ok "Status geaendert."; Read-Host "Enter"; - } - "2" { - $username = Read-Host "Benutzername"; - if ([string]::IsNullOrWhiteSpace($username) -or $username -notmatch '^[A-Za-z0-9._@-]{1,128}$') { Write-Fail "Ungueltiger Benutzername."; Read-Host "Enter"; continue; } - $existing = @($config.Smtp.AuthUsers) | Where-Object { ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1; - if ($existing) { Write-Fail "Benutzer existiert bereits."; Read-Host "Enter"; continue; } - - $plain1 = Read-Host "Passwort"; - $plain2 = Read-Host "Passwort wiederholen"; - if ($plain1.Length -lt 8 -or $plain1 -cne $plain2) { Write-Fail "Passwort ungueltig (mind. 8 Zeichen oder stimmen nicht ueberein)."; Read-Host "Enter"; continue; } - - $record = New-SmtpPasswordRecord -Password ($plain1); - $newUser = [pscustomobject]@{ Username = $username; Salt = $record.Salt; PasswordHash = $record.PasswordHash; Iterations = $record.Iterations }; - $config.Smtp.AuthUsers = @($config.Smtp.AuthUsers) + @($newUser); - Save-SmtpAuthConfigAndRestart -Config ($config); - Write-Ok "Angelegt."; Read-Host "Enter"; - } - "3" { - if (@($config.Smtp.AuthUsers).Count -eq 0) { Write-Warn "Keine SMTP-Benutzer vorhanden."; } else { @($config.Smtp.AuthUsers) | Format-Table Username,Iterations -AutoSize; } - Read-Host "Enter"; - } - "4" { - $username = Read-Host "Benutzername"; - $user = @($config.Smtp.AuthUsers) | Where-Object { ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) } | Select-Object -First 1; - if (-not $user) { Write-Fail "Benutzer nicht gefunden."; Read-Host "Enter"; continue; } - - $plain1 = Read-Host "Neues Passwort"; - $plain2 = Read-Host "Passwort wiederholen"; - if ($plain1.Length -lt 8 -or $plain1 -cne $plain2) { Write-Fail "Passwort ungueltig (mind. 8 Zeichen oder stimmen nicht ueberein)."; Read-Host "Enter"; continue; } - - $record = New-SmtpPasswordRecord -Password ($plain1); - $user.Salt = $record.Salt; $user.PasswordHash = $record.PasswordHash; $user.Iterations = $record.Iterations; - Save-SmtpAuthConfigAndRestart -Config ($config); - Write-Ok "Geaendert."; Read-Host "Enter"; - } - "5" { - $username = Read-Host "Benutzername"; - $found = @($config.Smtp.AuthUsers) | Where-Object { ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) }; - if (-not $found) { Write-Fail "Benutzer nicht gefunden."; Read-Host "Enter"; continue; } - if (Confirm-Yes "Loeschen?") { - $config.Smtp.AuthUsers = @($config.Smtp.AuthUsers | Where-Object { -not ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) }); - Save-SmtpAuthConfigAndRestart -Config ($config); - Write-Ok "Geloescht."; - } - Read-Host "Enter"; - } - "6" { - $current = @($config.Smtp.AllowUnauthenticatedNetworks) -join ","; - $input = Read-Default "Netze/IPs ohne AUTH" $(if ($current) {$current } else { "-" }); - if ($input -eq "-") { $config.Smtp.AllowUnauthenticatedNetworks = @(); } else { $config.Smtp.AllowUnauthenticatedNetworks = @($input -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ }); } - Save-SmtpAuthConfigAndRestart -Config ($config); - } - "7" { - $value = Read-Host "Max. Fehlversuche"; - if ($value -match '^\d+$') { $config.Smtp.AuthMaxFailures = [int]$value; Save-SmtpAuthConfigAndRestart -Config ($config); } - } - "0" { return; } - } - $config = Get-RelayConfig -TargetPath ($InstallPath); - $config = Ensure-SmtpAuthConfig -Config ($config); - } -} - -function Invoke-FailureModeTests { - $tester = Join-Path ($InstallPath) "Test-SMTPGraphRelay-FailureModes.ps1" - if (Test-Path -LiteralPath ($tester)) { & $tester -InstallPath ($InstallPath) } -} - -function Uninstall-Relay { - Write-Title "SMTPGraphRelay - Deinstallation" - $config = Get-RelayConfig -TargetPath ($InstallPath) - if (-not (Confirm-Yes "Wirklich entfernen?")) { return } - - Stop-RelayTask - Unregister-ScheduledTask -TaskName ($TaskName) -Confirm:$false -ErrorAction SilentlyContinue - Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "SMTPGraphRelay TCP *" } | Remove-NetFirewallRule -ErrorAction SilentlyContinue - - if ($config -and$config.Graph.CertificateThumbprint -and (Confirm-Yes "Zertifikat entfernen?")) { - Remove-Item -LiteralPath "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)" -Force -ErrorAction SilentlyContinue - } - if ($config -and (Confirm-Yes "Entra-App entfernen?")) { - Ensure-Modules - Connect-RelayGraphAdmin -TenantId ($config.Graph.TenantId) - try { - $app = Get-MgApplication -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id" | Select-Object -First 1 - $sp = Get-MgServicePrincipal -Filter "appId eq '$($config.Graph.ClientId)'" -Property "id" | Select-Object -First 1 - if ($sp) { Remove-MgServicePrincipal -ServicePrincipalId ($sp.Id) -ErrorAction SilentlyContinue } - if ($app) { Remove-MgApplication -ApplicationId ($app.Id) -ErrorAction SilentlyContinue } - } finally { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } - } - - $currentInstaller = [IO.Path]::GetFullPath($PSCommandPath) - foreach ($item in Get-ChildItem -LiteralPath ($InstallPath) -Force -ErrorAction SilentlyContinue) { - if ($item.FullName -ne $currentInstaller) { Remove-Item -LiteralPath ($item.FullName) -Recurse -Force -ErrorAction SilentlyContinue } - } - Write-Ok "Programmdateien entfernt." -} - -function Show-Status { - Write-Title "SMTPGraphRelay - Status anzeigen" - $configPath = Join-Path ($InstallPath)$ConfigFileName - Write-Host "Installationspfad: $InstallPath" - - if (Test-Path -LiteralPath ($configPath)) { - try { - $config = Get-RelayConfig -TargetPath ($InstallPath) - Write-Host " Sender: $($config.Graph.SenderMailbox)" - Write-Host " SMTP: $($config.Smtp.ListenAddress):$($config.Smtp.Port)" - Write-Host " AUTH: $([bool]$config.Smtp.RequireAuth)" - Write-Host " Failed: $(@(Get-FailedQueueEntries -Config ($config)).Count) Mail(s)" - } catch {} - } - $task = Get-ScheduledTask -TaskName ($TaskName) -ErrorAction SilentlyContinue - if ($task) { Write-Host "Task State: $($task.State)" } -} - -function Show-Menu { - Clear-Host - Write-Title "SMTPGraphRelay - Verwaltung" - Write-Host " [1] Neuinstallation aus Gitea" - Write-Host " [2] Installation reparieren" - Write-Host " [3] Online Update" - Write-Host " [4] Entra Berechtigungen pruefen" - Write-Host " [5] Zertifikat erneuern" - Write-Host " [6] Health Check" - Write-Host " [7] Deinstallieren" - Write-Host " [8] Status anzeigen" - Write-Host " [9] SMTP-AUTH verwalten" - Write-Host " [10] Failed Queue" - Write-Host " [0] Beenden`n" -} - -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 } "0" { break } - } - } catch { Write-Fail $_.Exception.Message } - if ($choice -ne "0") { Read-Host "`nEnter druecken..." } - if ($choice -eq "0") { break } -} \ No newline at end of file