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
|
```text
|
||||||
- privater Schlüssel vorhanden
|
MaxFileSizeMB = 10
|
||||||
- Ablaufdatum
|
RetentionDays = 30
|
||||||
|
CleanupHours = 12
|
||||||
|
```
|
||||||
|
|
||||||
Standardwerte:
|
Das aktive Log bleibt:
|
||||||
|
|
||||||
- Warnung ab 60 Tagen Restlaufzeit
|
```text
|
||||||
- kritisch ab 14 Tagen
|
logs\SMTPGraphRelay.log
|
||||||
- erneute Prüfung alle 12 Stunden
|
```
|
||||||
|
|
||||||
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
|
```json
|
||||||
"Graph": {
|
"Logging": {
|
||||||
"TenantId": "...",
|
"MaxFileSizeMB": 10,
|
||||||
"ClientId": "...",
|
"RetentionDays": 30,
|
||||||
"CertificateThumbprint": "...",
|
"CleanupHours": 12
|
||||||
"SenderMailbox": "info@example.com",
|
|
||||||
"ForceSender": true,
|
|
||||||
"CertificateWarningDays": 60,
|
|
||||||
"CertificateCriticalDays": 14,
|
|
||||||
"CertificateCheckHours": 12
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Die zusätzlichen Werte sind optional; bestehende Configs funktionieren unverändert.
|
Beispiel:
|
||||||
|
|
||||||
## Rotation
|
```json
|
||||||
|
{
|
||||||
`Renew-SMTPGraphRelayCertificate.ps1` muss als Administrator mit
|
"Smtp": {
|
||||||
**Windows PowerShell 5.1** ausgeführt werden.
|
"ListenAddress": "0.0.0.0",
|
||||||
|
"Port": 2525,
|
||||||
```powershell
|
"Hostname": "SMTPRELAY01",
|
||||||
cd "C:\Program Files\SMTPGraphRelay"
|
"AllowedNetworks": [
|
||||||
.\Renew-SMTPGraphRelayCertificate.ps1
|
"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
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Ablauf:
|
## Parallel-sicher
|
||||||
|
|
||||||
1. bestehende Config lesen
|
V1.5 verwendet weiterhin den benannten Mutex:
|
||||||
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:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
OLD funktioniert
|
Local\SMTPGraphRelay-Log
|
||||||
|
|
|
||||||
NEW erzeugen
|
|
||||||
|
|
|
||||||
OLD + NEW in Entra
|
|
||||||
|
|
|
||||||
NEW Auth testen
|
|
||||||
|
|
|
||||||
Config -> NEW
|
|
||||||
|
|
|
||||||
Relay neu starten
|
|
||||||
|
|
|
||||||
erst dann optional OLD entfernen
|
|
||||||
```
|
```
|
||||||
|
|
||||||
So gibt es während der Rotation keinen Zeitpunkt ohne gültiges Credential und ein
|
Rotation und Schreiben passieren innerhalb desselben Locks. Dadurch können parallele
|
||||||
Rollback bleibt möglich.
|
SMTP-Runspaces und der Queue-/Graph-Worker nicht gleichzeitig dieselbe Logdatei
|
||||||
|
rotieren oder beschreiben.
|
||||||
|
|
||||||
## Update von V1.3
|
## Update von V1.4
|
||||||
|
|
||||||
Task stoppen:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Stop-ScheduledTask -TaskName "SMTPGraphRelay"
|
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
|
```powershell
|
||||||
Start-ScheduledTask -TaskName "SMTPGraphRelay"
|
Start-ScheduledTask -TaskName "SMTPGraphRelay"
|
||||||
```
|
```
|
||||||
|
|
||||||
Die `config.json` muss nicht geändert werden.
|
Im Log sollte beim Start u. a. erscheinen:
|
||||||
|
|
||||||
Im Log erscheint bei einem gesunden Zertifikat beim Start beispielsweise:
|
|
||||||
|
|
||||||
```text
|
```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
|
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.
|
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()]
|
[CmdletBinding()]
|
||||||
@@ -18,6 +18,114 @@ param(
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[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 {
|
function Write-Log {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$Message,
|
[Parameter(Mandatory)][string]$Message,
|
||||||
@@ -31,28 +139,42 @@ function Write-Log {
|
|||||||
try {
|
try {
|
||||||
if ($script:Config -and $script:Config.Paths.Logs) {
|
if ($script:Config -and $script:Config.Paths.Logs) {
|
||||||
$logDir = $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
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
$logFile = Join-Path $logDir "SMTPGraphRelay.log"
|
$logFile = Join-Path $logDir "SMTPGraphRelay.log"
|
||||||
|
|
||||||
# Mehrere SMTP-Runspaces und der Queue-Worker können gleichzeitig loggen.
|
# 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")
|
$mutex = New-Object System.Threading.Mutex($false, "Local\SMTPGraphRelay-Log")
|
||||||
$lockTaken = $false
|
$lockTaken = $false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$lockTaken = $mutex.WaitOne(5000)
|
$lockTaken = $mutex.WaitOne(5000)
|
||||||
|
|
||||||
if ($lockTaken) {
|
if ($lockTaken) {
|
||||||
|
Rotate-LogIfNeeded -LogFile $logFile
|
||||||
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
|
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
|
||||||
|
Invoke-LogMaintenance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if ($lockTaken) {
|
if ($lockTaken) {
|
||||||
try { $mutex.ReleaseMutex() } catch {}
|
try { $mutex.ReleaseMutex() } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
$mutex.Dispose()
|
$mutex.Dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
}
|
||||||
|
catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Resolve-PathFromConfig {
|
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
|
New-Item -ItemType Directory -Path (Resolve-PathFromConfig $script:Config.Paths.Logs) -Force | Out-Null
|
||||||
Initialize-QueueDirectories
|
Initialize-QueueDirectories
|
||||||
|
|
||||||
|
# Alte Logarchive beim Start direkt bereinigen.
|
||||||
|
Invoke-LogMaintenance -Force
|
||||||
|
|
||||||
# Zertifikat beim Start immer prüfen und Status protokollieren.
|
# Zertifikat beim Start immer prüfen und Status protokollieren.
|
||||||
[void](Test-RelayCertificateExpiry -ForceLog)
|
[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
|
# Nur die Funktionen, die ein SMTP-Client wirklich benötigt, werden in die
|
||||||
# Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker.
|
# Worker-Runspaces kopiert. Graph-/Queue-Versand bleibt in einem separaten Worker.
|
||||||
$smtpFunctionNames = @(
|
$smtpFunctionNames = @(
|
||||||
|
"Get-LogSettings",
|
||||||
|
"Invoke-LogMaintenance",
|
||||||
|
"Rotate-LogIfNeeded",
|
||||||
"Write-Log",
|
"Write-Log",
|
||||||
"Resolve-PathFromConfig",
|
"Resolve-PathFromConfig",
|
||||||
"Test-IPv4InCidr",
|
"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
|
# Damit ein langsamer Graph-Aufruf nicht mehr die Annahme neuer SMTP-Verbindungen
|
||||||
# blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace.
|
# blockiert, läuft Process-Queue dauerhaft in einem eigenen Runspace.
|
||||||
$queueFunctionNames = @(
|
$queueFunctionNames = @(
|
||||||
|
"Get-LogSettings",
|
||||||
|
"Invoke-LogMaintenance",
|
||||||
|
"Rotate-LogIfNeeded",
|
||||||
"Write-Log",
|
"Write-Log",
|
||||||
"Resolve-PathFromConfig",
|
"Resolve-PathFromConfig",
|
||||||
"Get-GraphConnection",
|
"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 = [System.Net.Sockets.TcpListener]::new($listenIp, [int]$script:Config.Smtp.Port)
|
||||||
$listener.Start()
|
$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 "Graph-Absender: $($script:Config.Graph.SenderMailbox)"
|
||||||
Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"
|
Write-Log "Maximale parallele SMTP-Verbindungen: $script:MaxConcurrentClients"
|
||||||
Write-Log "Queue-/Graph-Worker läuft separat vom SMTP-Listener."
|
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 {
|
try {
|
||||||
while ($true) {
|
while ($true) {
|
||||||
Remove-CompletedSmtpWorkers
|
Remove-CompletedSmtpWorkers
|
||||||
|
|||||||
Reference in New Issue
Block a user