Dateien nach "/" hochladen

This commit is contained in:
2026-08-13 23:40:25 +02:00
parent c0c333b053
commit b13442830b
+308 -195
View File
@@ -151,6 +151,92 @@ function Ensure-Modules {
} }
} }
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 `
-LiteralPath $source `
-Destination (Join-Path $TargetPath $name) `
-Force
Write-Ok "Installiert: $name"
}
}
}
function Get-SourceFile { function Get-SourceFile {
param([Parameter(Mandatory)][string]$Name) param([Parameter(Mandatory)][string]$Name)
@@ -691,200 +777,185 @@ function Invoke-PostUpdateHealthCheck {
} }
function Install-New { function Install-New {
Write-Title "SMTPGraphRelay - Neuinstallation" Write-Title "SMTPGraphRelay - Neuinstallation aus Gitea"
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 würde eine neue App/Zertifikat erzeugen."
if (-not (Confirm-Yes "Trotzdem fortfahren?")) { if (-not (Confirm-Yes "Trotzdem fortfahren?")) {
return return
} }
} }
Ensure-Modules -IncludeExchange $staging = $null
Copy-ProgramFiles -TargetPath $InstallPath -RequireRelay
Ensure-Directories -TargetPath $InstallPath
$packageVersion = Get-PackageVersion
if ($packageVersion -and $packageVersion.Version) {
Write-Ok "Installiere Paketversion $($packageVersion.Version)."
}
$appName = Read-Default "Name der Entra App" $AppDefaultName
$senderMailbox = Read-Host "M365-Absenderpostfach (z.B. info@firma.de)"
while ([string]::IsNullOrWhiteSpace($senderMailbox) -or $senderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') {
$senderMailbox = Read-Host "Bitte eine gültige 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 { try {
$tenantId = (Get-MgContext).TenantId $staging = New-RepoStagingArea
Write-Ok "Tenant: $tenantId"
$appParams = @{ Ensure-Modules -IncludeExchange
DisplayName = $appName Ensure-Directories -TargetPath $InstallPath
SignInAudience = "AzureADMyOrg"
KeyCredentials = @( # Nur Programmdateien aus Git übernehmen.
(Convert-CertToKeyCredential -Certificate $cert) Install-RepoProgramFiles `
) -ReleaseRoot $staging.ReleaseRoot `
-TargetPath $InstallPath
Write-Ok "Programmdateien aus Gitea installiert."
$appName = Read-Default "Name der Entra App" $AppDefaultName
$senderMailbox = Read-Host "M365-Absenderpostfach (z.B. info@firma.de)"
while ([string]::IsNullOrWhiteSpace($senderMailbox) -or $senderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') {
$senderMailbox = Read-Host "Bitte eine gültige Mailadresse eingeben"
} }
$app = New-MgApplication -BodyParameter $appParams $listenAddress = Read-Default "Lokale Listen-IP" "0.0.0.0"
Write-Ok "App Registration erstellt: $($app.AppId)" $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 { $_ })
$sp = $null Write-Info "Erzeuge Relay-Zertifikat..."
for ($i = 0; $i -lt 10 -and -not $sp; $i++) { $cert = New-RelayCertificate
try { Write-Ok "Zertifikat erstellt: $($cert.Thumbprint)"
$sp = New-MgServicePrincipal -AppId $app.AppId
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)
)
} }
catch {
Start-Sleep -Seconds 2
}
}
if (-not $sp) { $app = New-MgApplication -BodyParameter $appParams
throw "Entra Service Principal konnte nicht erstellt werden." Write-Ok "App Registration erstellt: $($app.AppId)"
}
Write-Ok "Entra Service Principal erstellt: $($sp.Id)" $sp = $null
for ($i = 0; $i -lt 10 -and -not $sp; $i++) {
# Keine globale Graph Mail.Send Permission! try {
Test-NoGlobalMailSend -ServicePrincipalObjectId $sp.Id $sp = New-MgServicePrincipal -AppId $app.AppId
Ensure-ExchangeRbac `
-TenantId $tenantId `
-ClientId $app.AppId `
-ServicePrincipalObjectId $sp.Id `
-AppName $appName `
-SenderMailbox $senderMailbox
$config = [ordered]@{
Smtp = [ordered]@{
ListenAddress = $listenAddress
Port = $port
Hostname = $env:COMPUTERNAME
AllowedNetworks = $allowedNetworks
MaxMessageSizeMB = 25
ClientTimeoutSeconds = 120
MaxConcurrentClients = 20
}
Graph = [ordered]@{
TenantId = $tenantId
ClientId = $app.AppId
CertificateThumbprint = $cert.Thumbprint
SenderMailbox = $senderMailbox
ForceSender = $true
CertificateWarningDays = 60
CertificateCriticalDays = 14
CertificateCheckHours = 12
}
Queue = [ordered]@{
PollSeconds = 10
MaxRetries = 8
RetryMinutes = @(1,5,15,30,60,120,240,480)
}
Paths = [ordered]@{
Queue = "queue"
Failed = "failed"
Logs = "logs"
}
Logging = [ordered]@{
MaxFileSizeMB = 10
RetentionDays = 30
CleanupHours = 12
}
}
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 bestätigt werden."
}
Write-Ok "App-only Anmeldung funktioniert."
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Start-RelayTask
Write-Title "Neuinstallation abgeschlossen"
Write-Host "Installationspfad: $InstallPath"
$packageVersion = Get-PackageVersion
$installedVersion = Get-InstalledVersion -TargetPath $InstallPath
if ($packageVersion -and $packageVersion.Version) {
Write-Host "Paketversion: $($packageVersion.Version)"
}
if ($installedVersion -and $installedVersion.Version) {
Write-Host "Installierte Version: $($installedVersion.Version)"
}
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) { catch {
Write-Ok "Version ist aktuell." Start-Sleep -Seconds 2
}
else {
Write-Warn "Lokale Version ist neuer als Repository-main."
} }
} }
if (-not $sp) {
throw "Entra Service Principal konnte nicht erstellt werden."
}
Write-Ok "Entra Service Principal erstellt: $($sp.Id)"
Test-NoGlobalMailSend -ServicePrincipalObjectId $sp.Id
Ensure-ExchangeRbac `
-TenantId $tenantId `
-ClientId $app.AppId `
-ServicePrincipalObjectId $sp.Id `
-AppName $appName `
-SenderMailbox $senderMailbox
$config = [ordered]@{
Smtp = [ordered]@{
ListenAddress = $listenAddress
Port = $port
Hostname = $env:COMPUTERNAME
AllowedNetworks = $allowedNetworks
MaxMessageSizeMB = 25
ClientTimeoutSeconds = 120
MaxConcurrentClients = 20
}
Graph = [ordered]@{
TenantId = $tenantId
ClientId = $app.AppId
CertificateThumbprint = $cert.Thumbprint
SenderMailbox = $senderMailbox
ForceSender = $true
CertificateWarningDays = 60
CertificateCriticalDays = 14
CertificateCheckHours = 12
}
Queue = [ordered]@{
PollSeconds = 10
MaxRetries = 8
RetryMinutes = @(1,5,15,30,60,120,240,480)
}
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 bestätigt 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 abschließenden Health Check..."
& $health -ConfigPath (Join-Path $InstallPath $ConfigFileName)
$healthExit = $LASTEXITCODE
if ($healthExit -ge 2) {
Write-Warn "Installation abgeschlossen, Health Check meldet Fehler. Bitte Ausgabe prüfen."
}
}
Write-Title "Neuinstallation abgeschlossen"
Write-Host "Version: $($staging.VersionInfo.Version)"
Write-Host "Installationspfad: $InstallPath"
Write-Host "Client ID: $($app.AppId)"
Write-Host "Tenant ID: $tenantId"
Write-Host "Sender: $senderMailbox"
Write-Host "SMTP: $listenAddress`:$port"
}
finally {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
} }
}
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"
Write-Host "SMTP: $listenAddress`:$port"
} }
finally { finally {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Remove-RepoStagingArea -Staging $staging
} }
} }
function Repair-Installation { function Repair-Installation {
Write-Title "SMTPGraphRelay - Repair" Write-Title "SMTPGraphRelay - Repair aus Gitea"
$config = Get-RelayConfig -TargetPath $InstallPath $config = Get-RelayConfig -TargetPath $InstallPath
if (-not $config) { if (-not $config) {
@@ -893,38 +964,80 @@ function Repair-Installation {
return return
} }
Ensure-Modules $staging = $null
Copy-ProgramFiles -TargetPath $InstallPath $backupPath = $null
Ensure-Directories -TargetPath $InstallPath
$certPath = "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)" try {
$cert = Get-Item -LiteralPath $certPath -ErrorAction SilentlyContinue $staging = New-RepoStagingArea
if (-not $cert) { Ensure-Modules
Write-Fail "Konfiguriertes Zertifikat fehlt: $($config.Graph.CertificateThumbprint)" Ensure-Directories -TargetPath $InstallPath
Write-Warn "Repair erzeugt absichtlich kein neues Credential. Nutze Zertifikat erneuern."
$certPath = "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)"
$cert = Get-Item -LiteralPath $certPath -ErrorAction SilentlyContinue
if (-not $cert) {
Write-Fail "Konfiguriertes Zertifikat fehlt: $($config.Graph.CertificateThumbprint)"
Write-Warn "Repair erzeugt absichtlich kein neues Credential. Nutze Zertifikat erneuern."
}
elseif (-not $cert.HasPrivateKey) {
Write-Fail "Konfiguriertes Zertifikat besitzt keinen privaten Schlüssel."
}
else {
Write-Ok "Zertifikat vorhanden und besitzt Private Key."
}
Write-Info "Sichere aktuelle Programmdateien..."
$backupPath = Backup-ManagedFiles -TargetPath $InstallPath
Write-Ok "Backup: $backupPath"
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 (ExitCode $healthExit)."
}
if ($healthExit -eq 1) {
Write-Warn "Repair abgeschlossen, Health Check enthält Warnungen."
}
else {
Write-Ok "Repair Health Check erfolgreich."
}
Write-Title "Repair abgeschlossen"
Write-Host "Installierte Repo-Version: $($staging.VersionInfo.Version)"
} }
elseif (-not $cert.HasPrivateKey) { catch {
Write-Fail "Konfiguriertes Zertifikat besitzt keinen privaten Schlüssel." Write-Fail "Repair fehlgeschlagen: $($_.Exception.Message)"
if ($backupPath -and (Test-Path -LiteralPath $backupPath)) {
Write-Warn "Stelle vorherige Programmdateien wieder her..."
try {
Stop-RelayTask
Restore-ManagedFiles -BackupPath $backupPath -TargetPath $InstallPath
Ensure-ScheduledTask -TargetPath $InstallPath
Start-RelayTask
Write-Ok "Rollback nach Repair abgeschlossen."
}
catch {
Write-Fail "Repair-Rollback fehlgeschlagen: $($_.Exception.Message)"
}
}
} }
else { finally {
Write-Ok "Zertifikat vorhanden und besitzt Private Key." Remove-RepoStagingArea -Staging $staging
} }
Ensure-FirewallRule -Port ([int]$config.Smtp.Port)
Ensure-ScheduledTask -TargetPath $InstallPath
Start-RelayTask
$health = Join-Path $InstallPath $HealthFileName
if (Test-Path -LiteralPath $health) {
Write-Info "Starte Health Check..."
& $health -ConfigPath (Join-Path $InstallPath $ConfigFileName)
$healthExit = $LASTEXITCODE
Write-Info "Health Check ExitCode: $healthExit"
}
Write-Title "Repair abgeschlossen"
} }
function Update-Relay { function Update-Relay {
@@ -1338,7 +1451,7 @@ function Show-Status {
function Show-Menu { function Show-Menu {
Clear-Host Clear-Host
Write-Title "SMTPGraphRelay - Installer / Repair / 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
@@ -1349,8 +1462,8 @@ function Show-Menu {
Write-Host "Updatequelle: Gitea / main" -ForegroundColor DarkGray Write-Host "Updatequelle: Gitea / main" -ForegroundColor DarkGray
Write-Host "" Write-Host ""
Write-Host " [1] Neuinstallation" Write-Host " [1] Neuinstallation aus Gitea"
Write-Host " [2] Installation 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 / Exchange RBAC prüfen" Write-Host " [4] Entra / Exchange RBAC prüfen"
Write-Host " [5] Zertifikat erneuern" Write-Host " [5] Zertifikat erneuern"