#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS SMTPGraphRelay Installer / Repair / Update .DESCRIPTION Einheitliches Verwaltungswerkzeug für SMTPGraphRelay. Modi: 1 - Neuinstallation 2 - Installation reparieren 3 - Relay aktualisieren 4 - Entra App & Berechtigungen prüfen 5 - Zertifikat erneuern 6 - Health Check ausführen 7 - Deinstallieren WICHTIG: Dieses Skript muss mit Windows PowerShell 5.1 ausgeführt werden. #> [CmdletBinding()] param( [string]$InstallPath = "$env:ProgramFiles\SMTPGraphRelay" ) $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" # Dateien, die ein Update verändern darf. # config.json, Queue, Logs und sonstige lokale Daten sind absichtlich NICHT enthalten. $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\vert{}ja\vert{}y\vert{}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 ausgeführt werden." Write-Host "" Write-Host "Aktuell erkannt:" Write-Host " Edition: $($PSVersionTable.PSEdition)" Write-Host " Version: $($PSVersionTable.PSVersion)" Write-Host "" Write-Host "Bitte starten:" Write-Host " C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ForegroundColor Yellow 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" ) foreach ($module in$modules) { $existing = Get-Module -ListAvailable -Name$module | Sort-Object Version -Descending | Select-Object -First 1 if (-not $existing) { Write-Info "$module fehlt. Installation für AllUsers..." Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber $existing = Get-Module -ListAvailable -Name$module | Sort-Object Version -Descending | Select-Object -First 1 } if (-not $existing) { throw "Modul '$module' konnte nicht installiert/gefunden werden." } # Schutz gegen den bereits beobachteten PowerShell-7-Pfad. 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." } } 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..." Write-Info "Quelle: $RemoteArchiveUrl" 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 # Pflichtdateien prüfen. foreach ($required in @("SMTPGraphRelay.ps1", "version.json")) { if (-not (Test-Path -LiteralPath (Join-Path $releaseRoot$required))) { throw "Repository-Archiv ist unvollständig: '$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 enthält 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 `