From 07db702a7a725ecde1de5db18298ba02a74f996e Mon Sep 17 00:00:00 2001 From: "manuel.maier" Date: Fri, 14 Aug 2026 00:14:20 +0200 Subject: [PATCH] Dateien nach "/" hochladen --- Setup-SMTPGraphRelay.ps1 | 416 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 416 insertions(+) diff --git a/Setup-SMTPGraphRelay.ps1 b/Setup-SMTPGraphRelay.ps1 index d06cead..f4e843e 100644 --- a/Setup-SMTPGraphRelay.ps1 +++ b/Setup-SMTPGraphRelay.ps1 @@ -873,6 +873,12 @@ function Install-New { MaxMessageSizeMB = 25 ClientTimeoutSeconds = 120 MaxConcurrentClients = 20 + MaxRecipients = 50 + MaxMessagesPerConnection = 25 + RequireAuth = $false + AuthMaxFailures = 5 + AllowUnauthenticatedNetworks = @() + AuthUsers = @() } Graph = [ordered]@{ TenantId = $tenantId @@ -888,6 +894,8 @@ function Install-New { PollSeconds = 10 MaxRetries = 8 RetryMinutes = @(1,5,15,30,60,120,240,480) + MaxPendingMessages = 5000 + MinFreeDiskSpaceMB = 1024 } Paths = [ordered]@{ Queue = "queue" @@ -1320,6 +1328,406 @@ function Invoke-HealthCheck { & $health -ConfigPath (Join-Path $InstallPath $ConfigFileName) } + +function ConvertTo-PlainText { + param([Parameter(Mandatory)][Security.SecureString]$SecureString) + + $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + + try { + return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) + } + finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) + } +} + +function Invoke-Pbkdf2Sha256 { + param( + [Parameter(Mandatory)][string]$Password, + [Parameter(Mandatory)][byte[]]$Salt, + [Parameter(Mandatory)][int]$Iterations, + [int]$Length = 32 + ) + + if ($Iterations -lt 1) { + throw "Iterations must be greater than zero." + } + + # Auf unterstützten .NET-Framework-Versionen verwenden wir die native, + # schnelle PBKDF2-SHA256-Implementierung. + 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 { + # Kompatibilitäts-Fallback für ältere .NET-Framework-Stände. + $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)][Security.SecureString]$Password) + + $plain = ConvertTo-PlainText -SecureString $Password + + try { + $salt = New-Object byte[] 16 + $rng = [Security.Cryptography.RandomNumberGenerator]::Create() + + try { + $rng.GetBytes($salt) + } + finally { + $rng.Dispose() + } + + $iterations = 150000 + $hash = Invoke-Pbkdf2Sha256 ` + -Password $plain ` + -Salt $salt ` + -Iterations $iterations ` + -Length 32 + + return [pscustomobject]@{ + Salt = [Convert]::ToBase64String($salt) + PasswordHash = [Convert]::ToBase64String($hash) + Iterations = $iterations + } + } + finally { + $plain = $null + } +} + +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 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 hinzufügen" + Write-Host " [3] Benutzer anzeigen" + Write-Host " [4] Passwort ändern" + Write-Host " [5] Benutzer löschen" + Write-Host " [6] Netze ohne Auth verwalten" + Write-Host " [7] Max. Fehlversuche ändern" + Write-Host " [0] Zurück" + Write-Host "" + + $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 "SMTP-AUTH Status geändert." + Read-Host "Enter" + } + + "2" { + $username = Read-Host "Benutzername" + + if ([string]::IsNullOrWhiteSpace($username) -or $username -notmatch '^[A-Za-z0-9._@-]{1,128}$') { + Write-Fail "Ungültiger 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 '$username' existiert bereits." + Read-Host "Enter" + continue + } + + $p1 = Read-Host "Passwort" -AsSecureString + $p2 = Read-Host "Passwort wiederholen" -AsSecureString + + $plain1 = ConvertTo-PlainText -SecureString $p1 + $plain2 = ConvertTo-PlainText -SecureString $p2 + + try { + if ($plain1.Length -lt 8) { + Write-Fail "Passwort muss mindestens 8 Zeichen lang sein." + Read-Host "Enter" + continue + } + + if ($plain1 -cne $plain2) { + Write-Fail "Passwörter stimmen nicht überein." + Read-Host "Enter" + continue + } + } + finally { + $plain1 = $null + $plain2 = $null + } + + $record = New-SmtpPasswordRecord -Password $p1 + + $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 "SMTP-Benutzer '$username' angelegt." + Read-Host "Enter" + } + + "3" { + Write-Host "" + + if (@($config.Smtp.AuthUsers).Count -eq 0) { + Write-Warn "Keine SMTP-Benutzer vorhanden." + } + else { + @($config.Smtp.AuthUsers) | + Select-Object Username,Iterations | + Format-Table -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 '$username' nicht gefunden." + Read-Host "Enter" + continue + } + + $p1 = Read-Host "Neues Passwort" -AsSecureString + $p2 = Read-Host "Passwort wiederholen" -AsSecureString + + $plain1 = ConvertTo-PlainText -SecureString $p1 + $plain2 = ConvertTo-PlainText -SecureString $p2 + + try { + if ($plain1.Length -lt 8) { + Write-Fail "Passwort muss mindestens 8 Zeichen lang sein." + Read-Host "Enter" + continue + } + + if ($plain1 -cne $plain2) { + Write-Fail "Passwörter stimmen nicht überein." + Read-Host "Enter" + continue + } + } + finally { + $plain1 = $null + $plain2 = $null + } + + $record = New-SmtpPasswordRecord -Password $p1 + $user.Salt = $record.Salt + $user.PasswordHash = $record.PasswordHash + $user.Iterations = $record.Iterations + + Save-SmtpAuthConfigAndRestart -Config $config + Write-Ok "Passwort für '$username' geändert." + 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 '$username' nicht gefunden." + Read-Host "Enter" + continue + } + + if (Confirm-Yes "Benutzer '$username' wirklich löschen?") { + $config.Smtp.AuthUsers = @( + $config.Smtp.AuthUsers | + Where-Object { -not ([string]$_.Username).Equals($username, [StringComparison]::OrdinalIgnoreCase) } + ) + + Save-SmtpAuthConfigAndRestart -Config $config + Write-Ok "Benutzer '$username' gelöscht." + } + + Read-Host "Enter" + } + + "6" { + $current = @($config.Smtp.AllowUnauthenticatedNetworks) -join "," + $input = Read-Default "Netze/IPs ohne AUTH, Komma getrennt; '-' für keine" $(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 + Write-Ok "Ausnahmen für SMTP-AUTH gespeichert." + Read-Host "Enter" + } + + "7" { + $value = Read-Host "Maximale Fehlversuche pro Verbindung [aktuell: $($config.Smtp.AuthMaxFailures)]" + + if ($value -match '^\d+$' -and [int]$value -ge 1 -and [int]$value -le 100) { + $config.Smtp.AuthMaxFailures = [int]$value + Save-SmtpAuthConfigAndRestart -Config $config + Write-Ok "Maximale Fehlversuche geändert." + } + else { + Write-Fail "Bitte einen Wert zwischen 1 und 100 eingeben." + } + + Read-Host "Enter" + } + + "0" { + return + } + + default { + Write-Warn "Ungültige Auswahl." + Start-Sleep -Seconds 1 + } + } + + # Config nach jeder Änderung neu laden, damit Serialisierung/Arrays exakt + # dem installierten Zustand entsprechen. + $config = Get-RelayConfig -TargetPath $InstallPath + $config = Ensure-SmtpAuthConfig -Config $config + } +} + function Uninstall-Relay { Write-Title "SMTPGraphRelay - Deinstallation" @@ -1436,6 +1844,12 @@ function Show-Status { Write-Host " Sender: $($config.Graph.SenderMailbox)" Write-Host " SMTP: $($config.Smtp.ListenAddress):$($config.Smtp.Port)" Write-Host " Client: $($config.Graph.ClientId)" + + if ($config.Smtp.PSObject.Properties.Name -contains "RequireAuth") { + $authText = if ([bool]$config.Smtp.RequireAuth) { "erforderlich" } else { "optional / aus" } + $authUsers = if ($config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { @($config.Smtp.AuthUsers).Count } else { 0 } + Write-Host " AUTH: $authText ($authUsers Benutzer)" + } } catch {} } else { @@ -1472,6 +1886,7 @@ function Show-Menu { Write-Host " [6] Health Check ausführen" Write-Host " [7] Deinstallieren" Write-Host " [8] Status anzeigen" + Write-Host " [9] SMTP-AUTH verwalten" Write-Host " [0] Beenden" Write-Host "" } @@ -1492,6 +1907,7 @@ while ($true) { "6" { Invoke-HealthCheck } "7" { Uninstall-Relay } "8" { Show-Status } + "9" { Manage-SmtpAuth } "0" { break } default { Write-Warn "Ungültige Auswahl." } }