Dateien nach "/" hochladen

This commit is contained in:
2026-08-13 23:03:02 +02:00
parent e4ceecad24
commit 4dee69639e
2 changed files with 393 additions and 57 deletions
+283 -14
View File
@@ -7,7 +7,7 @@
Nimmt lokale SMTP-Mails an, speichert sie als .eml in einer Queue und sendet sie
anschließend per Microsoft Graph sendMail mit App-only Zertifikatsauthentifizierung.
V1.2: robuste Queue, statuscodeabhängiger Graph-Retry, EHLO/HELO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT
V1.3: parallele SMTP-Clients, separater Queue-Worker, robuste Queue, statuscodeabhängiger Graph-Retry
#>
[CmdletBinding()]
@@ -33,7 +33,24 @@ function Write-Log {
$logDir = $script:Config.Paths.Logs
if (-not [IO.Path]::IsPathRooted($logDir)) { $logDir = Join-Path $PSScriptRoot $logDir }
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
Add-Content -LiteralPath (Join-Path $logDir "SMTPGraphRelay.log") -Value $line -Encoding UTF8
$logFile = Join-Path $logDir "SMTPGraphRelay.log"
# Mehrere SMTP-Runspaces und der Queue-Worker können gleichzeitig loggen.
# Ein benannter Mutex verhindert kollidierende Schreibzugriffe.
$mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log")
$lockTaken = $false
try {
$lockTaken = $mutex.WaitOne(5000)
if ($lockTaken) {
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
}
}
finally {
if ($lockTaken) {
try { $mutex.ReleaseMutex() } catch {}
}
$mutex.Dispose()
}
}
} catch {}
}
@@ -785,6 +802,120 @@ function Handle-SmtpClient {
}
}
function Get-FunctionBootstrap {
param(
[Parameter(Mandatory)]
[string[]]$FunctionNames
)
$parts = New-Object System.Collections.Generic.List[string]
foreach ($name in $FunctionNames) {
$item = Get-Item -LiteralPath ("Function:\{0}" -f $name) -ErrorAction Stop
$parts.Add(("function {0} {{`r`n{1}`r`n}}" -f $name, $item.Definition))
}
return ($parts -join "`r`n`r`n")
}
function New-WorkerConfig {
# Runspaces haben keinen verlässlichen $PSScriptRoot des Hauptskripts.
# Deshalb werden alle Pfade für Worker einmal absolut aufgelöst.
$copy = $script:Config | ConvertTo-Json -Depth 20 | ConvertFrom-Json
$copy.Paths.Queue = Resolve-PathFromConfig $script:Config.Paths.Queue
$copy.Paths.Failed = Resolve-PathFromConfig $script:Config.Paths.Failed
$copy.Paths.Logs = Resolve-PathFromConfig $script:Config.Paths.Logs
return $copy
}
function Send-ServiceBusyAndClose {
param(
[Parameter(Mandatory)]
[System.Net.Sockets.TcpClient]$Client
)
try {
$stream = $Client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII, 1024, $true)
$writer.NewLine = "`r`n"
$writer.AutoFlush = $true
$writer.WriteLine("421 4.3.2 SMTPGraphRelay busy, try again later")
$writer.Flush()
$writer.Dispose()
$stream.Dispose()
}
catch {}
finally {
try { $Client.Close() } catch {}
}
}
function Remove-CompletedSmtpWorkers {
for ($i = $script:ActiveSmtpWorkers.Count - 1; $i -ge 0; $i--) {
$worker = $script:ActiveSmtpWorkers[$i]
if ($worker.AsyncResult.IsCompleted) {
try {
[void]$worker.PowerShell.EndInvoke($worker.AsyncResult)
}
catch {
Write-Log ("SMTP-Worker für {0} wurde mit Fehler beendet: {1}" -f $worker.RemoteAddress, $_.Exception.Message) "WARN"
}
finally {
try { $worker.PowerShell.Dispose() } catch {}
$script:ActiveSmtpWorkers.RemoveAt($i)
}
}
}
}
function Start-SmtpClientWorker {
param(
[Parameter(Mandatory)]
[System.Net.Sockets.TcpClient]$Client
)
Remove-CompletedSmtpWorkers
$remoteAddress = "unknown"
try {
$remoteAddress = ([System.Net.IPEndPoint]$Client.Client.RemoteEndPoint).Address.ToString()
} catch {}
if ($script:ActiveSmtpWorkers.Count -ge $script:MaxConcurrentClients) {
Write-Log ("SMTP-Verbindung von {0} abgewiesen: Parallel-Limit {1} erreicht." -f $remoteAddress, $script:MaxConcurrentClients) "WARN"
Send-ServiceBusyAndClose -Client $Client
return
}
$ps = [System.Management.Automation.PowerShell]::Create()
$ps.RunspacePool = $script:SmtpRunspacePool
[void]$ps.AddScript($script:SmtpWorkerScript)
[void]$ps.AddArgument($Client)
[void]$ps.AddArgument($script:WorkerConfig)
try {
$async = $ps.BeginInvoke()
[void]$script:ActiveSmtpWorkers.Add([pscustomobject]@{
PowerShell = $ps
AsyncResult = $async
Client = $Client
RemoteAddress = $remoteAddress
StartedUtc = [DateTime]::UtcNow
})
}
catch {
try { $ps.Dispose() } catch {}
try { $Client.Close() } catch {}
throw
}
}
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "Konfiguration nicht gefunden: $ConfigPath. Bitte zuerst Setup-SMTPGraphRelay.ps1 ausführen."
}
@@ -794,33 +925,171 @@ $script:Config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | Conv
New-Item -ItemType Directory -Path (Resolve-PathFromConfig $script:Config.Paths.Logs) -Force | Out-Null
Initialize-QueueDirectories
# MaxConcurrentClients ist optional, damit bestehende config.json-Dateien unverändert weiterlaufen.
$script:MaxConcurrentClients = 20
if ($script:Config.Smtp.PSObject.Properties.Name -contains "MaxConcurrentClients") {
try {
$configuredMax = [int]$script:Config.Smtp.MaxConcurrentClients
if ($configuredMax -ge 1 -and $configuredMax -le 200) {
$script:MaxConcurrentClients = $configuredMax
}
else {
Write-Log "Ungültiges Smtp.MaxConcurrentClients; verwende Standard 20." "WARN"
}
}
catch {
Write-Log "Smtp.MaxConcurrentClients konnte nicht gelesen werden; verwende Standard 20." "WARN"
}
}
$script:WorkerConfig = New-WorkerConfig
# ---------------------------------------------------------------------------
# SMTP Runspace Pool
# ---------------------------------------------------------------------------
# Nur die Funktionen, die ein SMTP-Client wirklich benötigt, werden in die
# Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker.
$smtpFunctionNames = @(
"Write-Log",
"Resolve-PathFromConfig",
"Test-IPv4InCidr",
"Test-ClientAllowed",
"Get-QueueDirectories",
"Save-SmtpMessage",
"Write-SmtpLine",
"Handle-SmtpClient"
)
$smtpBootstrap = Get-FunctionBootstrap -FunctionNames $smtpFunctionNames
$script:SmtpWorkerScript = @"
param(`$Client, `$Config)
`$script:Config = `$Config
$smtpBootstrap
Handle-SmtpClient -Client `$Client
"@
$script:SmtpRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(
1,
$script:MaxConcurrentClients
)
$script:SmtpRunspacePool.Open()
$script:ActiveSmtpWorkers = New-Object System.Collections.ArrayList
# ---------------------------------------------------------------------------
# Separater Queue-/Graph-Worker
# ---------------------------------------------------------------------------
# Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen
# blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace.
$queueFunctionNames = @(
"Write-Log",
"Resolve-PathFromConfig",
"Get-GraphConnection",
"Set-MimeSender",
"Get-QueueDirectories",
"Get-GraphFailureInfo",
"Get-RetryDecision",
"Move-QueueItem",
"Send-QueuedMail",
"Process-Queue"
)
$queueBootstrap = Get-FunctionBootstrap -FunctionNames $queueFunctionNames
$queueWorkerScript = @"
param(`$Config)
`$script:Config = `$Config
$queueBootstrap
try {
while (`$true) {
try {
Process-Queue
}
catch {
Write-Log ("Queue-Worker: {0}" -f `$_.Exception.Message) "ERROR"
}
Start-Sleep -Seconds ([int]`$script:Config.Queue.PollSeconds)
}
}
finally {
try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {}
}
"@
$script:QueuePowerShell = [System.Management.Automation.PowerShell]::Create()
[void]$script:QueuePowerShell.AddScript($queueWorkerScript)
[void]$script:QueuePowerShell.AddArgument($script:WorkerConfig)
$script:QueueAsyncResult = $script:QueuePowerShell.BeginInvoke()
# ---------------------------------------------------------------------------
# Listener
# ---------------------------------------------------------------------------
$listenIp = [System.Net.IPAddress]::Parse($script:Config.Smtp.ListenAddress)
$listener = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port)
$listener.Start()
Write-Log "SMTPGraphRelay gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
Write-Log "SMTPGraphRelay V1.3 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)"
$lastQueueRun = [DateTime]::MinValue
Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"
Write-Log "Queue-/Graph-Worker läuft separat vom SMTP-Listener."
try {
while ($true) {
if ((Get-Date) -gt $lastQueueRun.AddSeconds([int]$script:Config.Queue.PollSeconds)) {
try { Process-Queue } catch { Write-Log "Queue-Worker: $($_.Exception.Message)" "ERROR" }
$lastQueueRun = Get-Date
Remove-CompletedSmtpWorkers
# Sollte der Queue-Worker unerwartet beendet werden, Relay nicht still
# ohne Versand weiterlaufen lassen.
if ($script:QueueAsyncResult.IsCompleted) {
try {
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
throw "Queue-Worker wurde unerwartet beendet."
}
catch {
throw "Queue-Worker wurde unerwartet beendet: $($_.Exception.Message)"
}
}
if ($listener.Pending()) {
$client = $listener.AcceptTcpClient()
# V1 verarbeitet Clients seriell. Für typische Geräte-/Monitoring-Relays bewusst simpel.
Handle-SmtpClient -Client $client
} else {
Start-Sleep -Milliseconds 200
Start-SmtpClientWorker -Client $client
}
else {
Start-Sleep -Milliseconds 100
}
}
}
finally {
$listener.Stop()
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Write-Log "SMTPGraphRelay wird beendet..." "INFO"
try { $listener.Stop() } catch {}
# Neue Clients werden nicht mehr angenommen; bestehende Sessions schließen.
foreach ($worker in @($script:ActiveSmtpWorkers)) {
try { $worker.Client.Close() } catch {}
try { $worker.PowerShell.Stop() } catch {}
try {
if ($worker.AsyncResult) {
[void]$worker.PowerShell.EndInvoke($worker.AsyncResult)
}
} catch {}
try { $worker.PowerShell.Dispose() } catch {}
}
$script:ActiveSmtpWorkers.Clear()
try { $script:SmtpRunspacePool.Close() } catch {}
try { $script:SmtpRunspacePool.Dispose() } catch {}
try { $script:QueuePowerShell.Stop() } catch {}
try {
if ($script:QueueAsyncResult) {
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
}
} catch {}
try { $script:QueuePowerShell.Dispose() } catch {}
try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {}
Write-Log "SMTPGraphRelay beendet."
}