diff --git a/Setup-SMTPGraphRelay.ps1 b/Setup-SMTPGraphRelay.ps1 index 54b10c0..0d3b45a 100644 --- a/Setup-SMTPGraphRelay.ps1 +++ b/Setup-SMTPGraphRelay.ps1 @@ -35,6 +35,23 @@ $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", + "Renew-SMTPGraphRelayCertificate.ps1", + "Setup-SMTPGraphRelay.ps1", + "version.json", + "README.md" +) + function Write-Title { param([string]$Text) Write-Host "" @@ -513,6 +530,166 @@ function Test-NoGlobalMailSend { } } + +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 enthält 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 + ) + + # Gitea kann beim Archiv einen zusätzlichen Root-Ordner erzeugen. + # Daher suchen wir nach der Kombination aus Relay + version.json statt + # einen konkreten Archivordnernamen vorauszusetzen. + $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 gültiges 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 unvollständig: '$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 Nachprüfung entfällt." + 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" @@ -670,6 +847,32 @@ function Install-New { elseif (Test-Path -LiteralPath (Join-Path $InstallPath $RelayFileName)) { Write-Warn "Installierte Version unbekannt (keine version.json)." } + + try { + $remoteVersionInfo = Get-RemoteVersion + if ($remoteVersionInfo -and $remoteVersionInfo.Version) { + Write-Host "Remote (main): $($remoteVersionInfo.Version)" + + if ($installedVersion -and $installedVersion.Version) { + $cmp = Compare-RelayVersions ` + -Installed ([string]$installedVersion.Version) ` + -Remote ([string]$remoteVersionInfo.Version) + + if ($cmp -gt 0) { + Write-Warn "Update verfügbar." + } + elseif ($cmp -eq 0) { + Write-Ok "Version ist aktuell." + } + else { + Write-Warn "Lokale Version ist neuer als Repository-main." + } + } + } + } + catch { + Write-Warn "Remote-Version konnte nicht geprüft werden." + } Write-Host "Client ID: $($app.AppId)" Write-Host "Tenant ID: $tenantId" Write-Host "Sender: $senderMailbox" @@ -725,7 +928,7 @@ function Repair-Installation { } function Update-Relay { - Write-Title "SMTPGraphRelay - Update" + Write-Title "SMTPGraphRelay - Online Update" $config = Get-RelayConfig -TargetPath $InstallPath if (-not $config) { @@ -734,76 +937,182 @@ function Update-Relay { return } - $packageVersion = Get-PackageVersion - $installedVersion = Get-InstalledVersion -TargetPath $InstallPath + $installedVersionInfo = Get-InstalledVersion -TargetPath $InstallPath + $installedVersion = $null - if ($packageVersion -and $packageVersion.Version) { - Write-Info "Paketversion: $($packageVersion.Version)" + if ($installedVersionInfo -and $installedVersionInfo.Version) { + $installedVersion = [string]$installedVersionInfo.Version + Write-Info "Installierte Version: $installedVersion" } - - if ($installedVersion -and $installedVersion.Version) { - Write-Info "Installierte Version: $($installedVersion.Version)" - - try { - $pkgVer = [version]$packageVersion.Version - $instVer = [version]$installedVersion.Version - - if ($pkgVer -lt $instVer) { - Write-Warn "Das Paket ist älter als die installierte Version." - if (-not (Confirm-Yes "Downgrade trotzdem durchführen?")) { - return - } - } - elseif ($pkgVer -eq $instVer) { - Write-Warn "Paket- und installierte Version sind identisch." - if (-not (Confirm-Yes "Version trotzdem erneut einspielen?")) { - return - } - } - } catch { - Write-Warn "Versionsvergleich konnte nicht durchgeführt werden." + else { + Write-Warn "Keine installierte version.json gefunden." + $installedVersion = Read-Host "Installierte Version manuell eingeben (z.B. 1.5.0)" + if ([string]::IsNullOrWhiteSpace($installedVersion)) { + Write-Fail "Ohne lokale Versionsinformation kann kein sicheres Online-Update durchgeführt werden." + return } } - $sourceRelay = Get-SourceFile -Name $RelayFileName - if (-not $sourceRelay) { - Write-Fail "Kein neues '$RelayFileName' neben dem Installer gefunden." + Write-Info "Prüfe Gitea auf neue Version..." + Write-Info "Repository: $RepoBaseUrl" + + try { + $remoteInfo = Get-RemoteVersion + } + catch { + Write-Fail "Remote-Version konnte nicht geladen werden: $($_.Exception.Message)" return } - Stop-RelayTask + $remoteVersion = [string]$remoteInfo.Version + Write-Ok "Remote-Version: $remoteVersion" - $installedRelay = Join-Path $InstallPath $RelayFileName - if (Test-Path -LiteralPath $installedRelay) { - $backup = Join-Path $InstallPath ("SMTPGraphRelay-backup-{0}.ps1" -f (Get-Date -Format "yyyyMMdd-HHmmss")) - Copy-Item -LiteralPath $installedRelay -Destination $backup -Force - Write-Ok "Backup erstellt: $backup" + try { + $comparison = Compare-RelayVersions -Installed $installedVersion -Remote $remoteVersion + } + catch { + Write-Fail $_.Exception.Message + return } - Copy-ProgramFiles -TargetPath $InstallPath -RequireRelay - Ensure-Directories -TargetPath $InstallPath - Ensure-FirewallRule -Port ([int]$config.Smtp.Port) - Ensure-ScheduledTask -TargetPath $InstallPath - Start-RelayTask + if ($comparison -eq 0) { + Write-Ok "SMTPGraphRelay ist bereits aktuell ($installedVersion)." + return + } - $health = Join-Path $InstallPath $HealthFileName - if (Test-Path -LiteralPath $health) { - Write-Info "Starte Health Check..." - & $health -ConfigPath (Join-Path $InstallPath $ConfigFileName) - $healthExit = $LASTEXITCODE - - if ($healthExit -eq 0) { - Write-Ok "Update-Health-Check erfolgreich." + if ($comparison -lt 0) { + Write-Warn "Die installierte Version ($installedVersion) ist neuer als main ($remoteVersion)." + if (-not (Confirm-Yes "Downgrade auf $remoteVersion durchführen?")) { + return } - elseif ($healthExit -eq 1) { - Write-Warn "Update abgeschlossen, Health Check enthält Warnungen." + } + else { + Write-Host "" + Write-Host "Update verfügbar:" -ForegroundColor Green + Write-Host " Installiert: $installedVersion" + Write-Host " Neu: $remoteVersion" -ForegroundColor Yellow + Write-Host "" + + if (-not (Confirm-Yes "Update auf $remoteVersion installieren?")) { + return + } + } + + Enable-Tls12 + + $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 + $taskWasRunning = $false + + try { + Write-Info "Lade Repository-Archiv..." + Invoke-WebRequest ` + -Uri $RemoteArchiveUrl ` + -OutFile $archivePath ` + -UseBasicParsing ` + -TimeoutSec 120 ` + -ErrorAction Stop + + if (-not (Test-Path -LiteralPath $archivePath)) { + throw "Download des Updatearchivs fehlgeschlagen." + } + + Write-Ok "Archiv heruntergeladen." + + Expand-Archive ` + -LiteralPath $archivePath ` + -DestinationPath $extractPath ` + -Force + + $releaseRoot = Find-ExtractedReleaseRoot -ExtractPath $extractPath + Write-Ok "Release im Archiv gefunden: $releaseRoot" + + $downloadedVersionInfo = Get-Content ` + -LiteralPath (Join-Path $releaseRoot "version.json") ` + -Raw ` + -Encoding UTF8 | ConvertFrom-Json + + if (-not $downloadedVersionInfo.Version) { + throw "version.json im Archiv enthält keine Version." + } + + if ([string]$downloadedVersionInfo.Version -ne $remoteVersion) { + throw "Versionskonflikt: version.json-URL meldet $remoteVersion, Archiv enthält $($downloadedVersionInfo.Version)." + } + + $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue + if ($task -and $task.State -eq "Running") { + $taskWasRunning = $true + } + + Write-Info "Erstelle Backup der verwalteten Programmdateien..." + $backupPath = Backup-ManagedFiles -TargetPath $InstallPath + Write-Ok "Backup: $backupPath" + + Stop-RelayTask + + Write-Info "Installiere Release $remoteVersion..." + 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 (ExitCode $healthExit)." + } + + if ($healthExit -eq 1) { + Write-Warn "Update erfolgreich, Health Check enthält Warnungen." } else { - Write-Fail "Update eingespielt, Health Check meldet Fehler. Backup liegt im Installationsordner." + Write-Ok "Health Check nach Update erfolgreich." + } + + Write-Title "Online Update abgeschlossen" + Write-Host "Vorher: $installedVersion" + Write-Host "Jetzt: $remoteVersion" -ForegroundColor Green + Write-Host "Backup: $backupPath" + } + catch { + Write-Host "" + Write-Fail "Update fehlgeschlagen: $($_.Exception.Message)" + + if ($backupPath -and (Test-Path -LiteralPath $backupPath)) { + Write-Warn "Automatischer Rollback wird durchgeführt..." + + try { + Stop-RelayTask + Restore-ManagedFiles ` + -BackupPath $backupPath ` + -TargetPath $InstallPath + + Ensure-ScheduledTask -TargetPath $InstallPath + Start-RelayTask + + Write-Ok "Rollback abgeschlossen." + } + catch { + Write-Fail "Rollback fehlgeschlagen: $($_.Exception.Message)" + Write-Warn "Backup liegt unter: $backupPath" + } } } - - Write-Title "Update abgeschlossen" + finally { + Remove-Item -LiteralPath $updateRoot -Recurse -Force -ErrorAction SilentlyContinue + } } function Verify-CloudRbac { @@ -1032,20 +1341,17 @@ function Show-Menu { Write-Title "SMTPGraphRelay - Installer / Repair / Update" Write-Host "Installationspfad: $InstallPath" -ForegroundColor DarkGray - $packageVersion = Get-PackageVersion $installedVersion = Get-InstalledVersion -TargetPath $InstallPath - if ($packageVersion -and $packageVersion.Version) { - Write-Host "Paketversion: $($packageVersion.Version)" -ForegroundColor DarkGray + if ($installedVersion -and $installedVersion.Version) { + Write-Host "Installiert: $($installedVersion.Version)" -ForegroundColor DarkGray } - 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" Write-Host " [2] Installation reparieren" - Write-Host " [3] Relay aktualisieren" + Write-Host " [3] Nach Online-Updates suchen" Write-Host " [4] Entra / Exchange RBAC prüfen" Write-Host " [5] Zertifikat erneuern" Write-Host " [6] Health Check ausführen"