Dateien nach "/" hochladen
This commit is contained in:
@@ -1,104 +1,137 @@
|
||||
# SMTPGraphRelay V1.4 – Zertifikats-Lifecycle
|
||||
# SMTPGraphRelay V1.5 – Log-Rotation
|
||||
|
||||
V1.4 erweitert V1.3 um Zertifikatsüberwachung und sichere Rotation.
|
||||
V1.5 erweitert V1.4 um automatische Log-Rotation und Log-Aufbewahrung.
|
||||
|
||||
## Relay: automatische Zertifikatsüberwachung
|
||||
## Standardverhalten
|
||||
|
||||
Beim Start wird das Zertifikat aus `Graph.CertificateThumbprint` geprüft:
|
||||
Ohne Änderungen an `config.json` gelten:
|
||||
|
||||
- Zertifikat vorhanden
|
||||
- privater Schlüssel vorhanden
|
||||
- Ablaufdatum
|
||||
```text
|
||||
MaxFileSizeMB = 10
|
||||
RetentionDays = 30
|
||||
CleanupHours = 12
|
||||
```
|
||||
|
||||
Standardwerte:
|
||||
Das aktive Log bleibt:
|
||||
|
||||
- Warnung ab 60 Tagen Restlaufzeit
|
||||
- kritisch ab 14 Tagen
|
||||
- erneute Prüfung alle 12 Stunden
|
||||
```text
|
||||
logs\SMTPGraphRelay.log
|
||||
```
|
||||
|
||||
Optional in `config.json`:
|
||||
Sobald es 10 MB erreicht, wird es z. B. umbenannt in:
|
||||
|
||||
```text
|
||||
logs\SMTPGraphRelay-20260813-231500.log
|
||||
```
|
||||
|
||||
Danach wird automatisch eine neue `SMTPGraphRelay.log` begonnen.
|
||||
|
||||
Archiv-Logs, die älter als 30 Tage sind, werden automatisch entfernt.
|
||||
|
||||
## Optionale Konfiguration
|
||||
|
||||
Die bestehende Config funktioniert unverändert.
|
||||
|
||||
Optional kann auf Root-Ebene ergänzt werden:
|
||||
|
||||
```json
|
||||
"Graph": {
|
||||
"Logging": {
|
||||
"MaxFileSizeMB": 10,
|
||||
"RetentionDays": 30,
|
||||
"CleanupHours": 12
|
||||
}
|
||||
```
|
||||
|
||||
Beispiel:
|
||||
|
||||
```json
|
||||
{
|
||||
"Smtp": {
|
||||
"ListenAddress": "0.0.0.0",
|
||||
"Port": 2525,
|
||||
"Hostname": "SMTPRELAY01",
|
||||
"AllowedNetworks": [
|
||||
"127.0.0.1/32",
|
||||
"10.0.0.0/8"
|
||||
],
|
||||
"MaxMessageSizeMB": 25,
|
||||
"ClientTimeoutSeconds": 120,
|
||||
"MaxConcurrentClients": 20
|
||||
},
|
||||
"Graph": {
|
||||
"TenantId": "...",
|
||||
"ClientId": "...",
|
||||
"CertificateThumbprint": "...",
|
||||
"SenderMailbox": "info@example.com",
|
||||
"ForceSender": true,
|
||||
"CertificateWarningDays": 60,
|
||||
"CertificateCriticalDays": 14,
|
||||
"CertificateCheckHours": 12
|
||||
"ForceSender": true
|
||||
},
|
||||
"Queue": {
|
||||
"PollSeconds": 10,
|
||||
"MaxRetries": 8,
|
||||
"RetryMinutes": [1,5,15,30,60,120,240,480]
|
||||
},
|
||||
"Paths": {
|
||||
"Queue": "queue",
|
||||
"Failed": "failed",
|
||||
"Logs": "logs"
|
||||
},
|
||||
"Logging": {
|
||||
"MaxFileSizeMB": 10,
|
||||
"RetentionDays": 30,
|
||||
"CleanupHours": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Die zusätzlichen Werte sind optional; bestehende Configs funktionieren unverändert.
|
||||
## Parallel-sicher
|
||||
|
||||
## Rotation
|
||||
|
||||
`Renew-SMTPGraphRelayCertificate.ps1` muss als Administrator mit
|
||||
**Windows PowerShell 5.1** ausgeführt werden.
|
||||
|
||||
```powershell
|
||||
cd "C:\Program Files\SMTPGraphRelay"
|
||||
.\Renew-SMTPGraphRelayCertificate.ps1
|
||||
```
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. bestehende Config lesen
|
||||
2. neues nicht exportierbares Zertifikat in `LocalMachine\My` erzeugen
|
||||
3. Entra-Admin interaktiv anmelden
|
||||
4. bestehende App über ihre ClientId finden
|
||||
5. neues Zertifikat **zusätzlich** in `keyCredentials` hinterlegen
|
||||
6. App-only Login mit dem neuen Zertifikat testen
|
||||
7. erst danach `config.json` atomar auf den neuen Thumbprint umstellen
|
||||
8. Relay-Scheduled-Task neu starten
|
||||
9. optional altes Cloud- und lokales Zertifikat entfernen
|
||||
|
||||
Exchange Application RBAC muss nicht neu aufgebaut werden, weil die App/ClientId
|
||||
bei der Rotation unverändert bleibt.
|
||||
|
||||
## Warum das alte Zertifikat zunächst erhalten bleibt
|
||||
|
||||
Die sichere Reihenfolge ist:
|
||||
V1.5 verwendet weiterhin den benannten Mutex:
|
||||
|
||||
```text
|
||||
OLD funktioniert
|
||||
|
|
||||
NEW erzeugen
|
||||
|
|
||||
OLD + NEW in Entra
|
||||
|
|
||||
NEW Auth testen
|
||||
|
|
||||
Config -> NEW
|
||||
|
|
||||
Relay neu starten
|
||||
|
|
||||
erst dann optional OLD entfernen
|
||||
Local\SMTPGraphRelay-Log
|
||||
```
|
||||
|
||||
So gibt es während der Rotation keinen Zeitpunkt ohne gültiges Credential und ein
|
||||
Rollback bleibt möglich.
|
||||
Rotation und Schreiben passieren innerhalb desselben Locks. Dadurch können parallele
|
||||
SMTP-Runspaces und der Queue-/Graph-Worker nicht gleichzeitig dieselbe Logdatei
|
||||
rotieren oder beschreiben.
|
||||
|
||||
## Update von V1.3
|
||||
|
||||
Task stoppen:
|
||||
## Update von V1.4
|
||||
|
||||
```powershell
|
||||
Stop-ScheduledTask -TaskName "SMTPGraphRelay"
|
||||
|
||||
Copy-Item `
|
||||
"C:\Program Files\SMTPGraphRelay\SMTPGraphRelay.ps1" `
|
||||
"C:\Program Files\SMTPGraphRelay\SMTPGraphRelay-V1.4-backup.ps1"
|
||||
```
|
||||
|
||||
V1.3 sichern und V1.4 als `SMTPGraphRelay.ps1` ablegen, dann:
|
||||
Dann V1.5 als:
|
||||
|
||||
```text
|
||||
C:\Program Files\SMTPGraphRelay\SMTPGraphRelay.ps1
|
||||
```
|
||||
|
||||
ablegen und:
|
||||
|
||||
```powershell
|
||||
Start-ScheduledTask -TaskName "SMTPGraphRelay"
|
||||
```
|
||||
|
||||
Die `config.json` muss nicht geändert werden.
|
||||
|
||||
Im Log erscheint bei einem gesunden Zertifikat beim Start beispielsweise:
|
||||
Im Log sollte beim Start u. a. erscheinen:
|
||||
|
||||
```text
|
||||
Graph-Zertifikat gültig bis 2028-08-13 23:00:00 (729 Tage verbleibend).
|
||||
SMTPGraphRelay V1.5 gestartet ...
|
||||
Log-Rotation: max. 10 MB pro Datei, Aufbewahrung 30 Tage.
|
||||
```
|
||||
|
||||
## Hinweis
|
||||
|
||||
Nur archivierte Dateien im Muster
|
||||
|
||||
```text
|
||||
SMTPGraphRelay-*.log
|
||||
```
|
||||
|
||||
werden automatisch gelöscht.
|
||||
|
||||
Die aktuell aktive `SMTPGraphRelay.log` wird nie durch die Retention-Regel gelöscht.
|
||||
|
||||
+140
-5
@@ -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.4: Zertifikatsüberwachung, parallele SMTP-Clients, separater Queue-Worker, robuste Queue und Graph-Retry
|
||||
V1.5: Log-Rotation, Zertifikatsüberwachung, parallele SMTP-Clients, Queue-Worker und robuste Graph-Retry-Logik
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
@@ -18,6 +18,114 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
function Get-LogSettings {
|
||||
$maxSizeMB = 10
|
||||
$retentionDays = 30
|
||||
$cleanupHours = 12
|
||||
|
||||
try {
|
||||
if ($script:Config.Logging) {
|
||||
if ($script:Config.Logging.PSObject.Properties.Name -contains "MaxFileSizeMB") {
|
||||
$v = [int]$script:Config.Logging.MaxFileSizeMB
|
||||
if ($v -ge 1 -and $v -le 1024) { $maxSizeMB = $v }
|
||||
}
|
||||
|
||||
if ($script:Config.Logging.PSObject.Properties.Name -contains "RetentionDays") {
|
||||
$v = [int]$script:Config.Logging.RetentionDays
|
||||
if ($v -ge 1 -and $v -le 3650) { $retentionDays = $v }
|
||||
}
|
||||
|
||||
if ($script:Config.Logging.PSObject.Properties.Name -contains "CleanupHours") {
|
||||
$v = [int]$script:Config.Logging.CleanupHours
|
||||
if ($v -ge 1 -and $v -le 168) { $cleanupHours = $v }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return [pscustomobject]@{
|
||||
MaxFileSizeMB = $maxSizeMB
|
||||
RetentionDays = $retentionDays
|
||||
CleanupHours = $cleanupHours
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-LogMaintenance {
|
||||
param(
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
if (-not $script:Config -or -not $script:Config.Paths.Logs) {
|
||||
return
|
||||
}
|
||||
|
||||
$settings = Get-LogSettings
|
||||
$logDir = $script:Config.Paths.Logs
|
||||
|
||||
if (-not [IO.Path]::IsPathRooted($logDir)) {
|
||||
if ($PSScriptRoot) {
|
||||
$logDir = Join-Path $PSScriptRoot $logDir
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||
|
||||
if (-not $script:NextLogCleanup) {
|
||||
$script:NextLogCleanup = [DateTime]::MinValue
|
||||
}
|
||||
|
||||
if (-not $Force -and (Get-Date) -lt $script:NextLogCleanup) {
|
||||
return
|
||||
}
|
||||
|
||||
$cutoff = (Get-Date).AddDays(-1 * $settings.RetentionDays)
|
||||
|
||||
foreach ($file in Get-ChildItem -LiteralPath $logDir -File -Filter "SMTPGraphRelay-*.log" -ErrorAction SilentlyContinue) {
|
||||
if ($file.LastWriteTime -lt $cutoff) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $file.FullName -Force -ErrorAction Stop
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
$script:NextLogCleanup = (Get-Date).AddHours($settings.CleanupHours)
|
||||
}
|
||||
|
||||
function Rotate-LogIfNeeded {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$LogFile
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $LogFile)) {
|
||||
return
|
||||
}
|
||||
|
||||
$settings = Get-LogSettings
|
||||
|
||||
try {
|
||||
$file = Get-Item -LiteralPath $LogFile -ErrorAction Stop
|
||||
$maxBytes = [int64]$settings.MaxFileSizeMB * 1024 * 1024
|
||||
|
||||
if ($file.Length -lt $maxBytes) {
|
||||
return
|
||||
}
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$dir = Split-Path $LogFile -Parent
|
||||
$archive = Join-Path $dir "SMTPGraphRelay-$stamp.log"
|
||||
|
||||
$counter = 1
|
||||
while (Test-Path -LiteralPath $archive) {
|
||||
$archive = Join-Path $dir ("SMTPGraphRelay-{0}-{1}.log" -f $stamp, $counter)
|
||||
$counter++
|
||||
}
|
||||
|
||||
Move-Item -LiteralPath $LogFile -Destination $archive -Force
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Message,
|
||||
@@ -31,28 +139,42 @@ function Write-Log {
|
||||
try {
|
||||
if ($script:Config -and $script:Config.Paths.Logs) {
|
||||
$logDir = $script:Config.Paths.Logs
|
||||
if (-not [IO.Path]::IsPathRooted($logDir)) { $logDir = Join-Path $PSScriptRoot $logDir }
|
||||
|
||||
if (-not [IO.Path]::IsPathRooted($logDir)) {
|
||||
if ($PSScriptRoot) {
|
||||
$logDir = Join-Path $PSScriptRoot $logDir
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||
$logFile = Join-Path $logDir "SMTPGraphRelay.log"
|
||||
|
||||
# Mehrere SMTP-Runspaces und der Queue-Worker können gleichzeitig loggen.
|
||||
# Ein benannter Mutex verhindert kollidierende Schreibzugriffe.
|
||||
# Ein benannter Mutex schützt Rotation und Schreibzugriff gemeinsam.
|
||||
$mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log")
|
||||
$lockTaken = $false
|
||||
|
||||
try {
|
||||
$lockTaken = $mutex.WaitOne(5000)
|
||||
|
||||
if ($lockTaken) {
|
||||
Rotate-LogIfNeeded -LogFile $logFile
|
||||
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
|
||||
Invoke-LogMaintenance
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($lockTaken) {
|
||||
try { $mutex.ReleaseMutex() } catch {}
|
||||
}
|
||||
|
||||
$mutex.Dispose()
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
function Resolve-PathFromConfig {
|
||||
@@ -999,6 +1121,9 @@ $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
|
||||
|
||||
# Alte Logarchive beim Start direkt bereinigen.
|
||||
Invoke-LogMaintenance -Force
|
||||
|
||||
# Zertifikat beim Start immer prüfen und Status protokollieren.
|
||||
[void](Test-RelayCertificateExpiry -ForceLog)
|
||||
|
||||
@@ -1039,6 +1164,9 @@ $script:WorkerConfig = New-WorkerConfig
|
||||
# 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 = @(
|
||||
"Get-LogSettings",
|
||||
"Invoke-LogMaintenance",
|
||||
"Rotate-LogIfNeeded",
|
||||
"Write-Log",
|
||||
"Resolve-PathFromConfig",
|
||||
"Test-IPv4InCidr",
|
||||
@@ -1072,6 +1200,9 @@ $script:ActiveSmtpWorkers = New-Object System.Collections.ArrayList
|
||||
# Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen
|
||||
# blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace.
|
||||
$queueFunctionNames = @(
|
||||
"Get-LogSettings",
|
||||
"Invoke-LogMaintenance",
|
||||
"Rotate-LogIfNeeded",
|
||||
"Write-Log",
|
||||
"Resolve-PathFromConfig",
|
||||
"Get-GraphConnection",
|
||||
@@ -1120,11 +1251,15 @@ $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 V1.3 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
|
||||
Write-Log "SMTPGraphRelay V1.5 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
|
||||
Write-Log "Graph-Absender: $($script:Config.Graph.SenderMailbox)"
|
||||
Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"
|
||||
Write-Log "Queue-/Graph-Worker läuft separat vom SMTP-Listener."
|
||||
|
||||
$logSettings = Get-LogSettings
|
||||
Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f `
|
||||
$logSettings.MaxFileSizeMB, $logSettings.RetentionDays)
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
Remove-CompletedSmtpWorkers
|
||||
|
||||
Reference in New Issue
Block a user