Dateien nach "/" hochladen
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
# SMTPGraphRelay 1.7.0
|
# SMTPGraphRelay 1.8.0
|
||||||
|
|
||||||
SMTPGraphRelay V1.7 ergänzt optionales lokales SMTP-AUTH für interne Clients.
|
V1.8 ergänzt zwei Betriebsfunktionen:
|
||||||
|
|
||||||
> **Hinweis:** `AUTH LOGIN` und `AUTH PLAIN` werden hier bewusst ohne TLS angeboten,
|
- Failed-Queue-Verwaltung im Setup-/Manager
|
||||||
> weil dieses Relay für vertrauenswürdige interne Netze gedacht ist. Die Zugangsdaten
|
- kooperativer Graceful Shutdown für Update, Repair und manuelles Stoppen über den Manager
|
||||||
> sind auf dem Netzwerkpfad daher nur Base64-kodiert, nicht verschlüsselt. Die
|
|
||||||
> bestehende IP-Allowlist sollte weiterhin restriktiv gesetzt bleiben.
|
Alle Funktionen aus V1.7 bleiben erhalten, insbesondere SMTP AUTH LOGIN/PLAIN,
|
||||||
|
Queue-ID, Retry/Backpressure, Zertifikatsrotation, Health Check und Online-Update.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -15,234 +16,141 @@ SMTPGraphRelay V1.7 ergänzt optionales lokales SMTP-AUTH für interne Clients.
|
|||||||
$u='https://me-gitea.maieredv.cloud/MAIEREDV/SMTPGraphRelay/raw/branch/main/Setup-SMTPGraphRelay.ps1';$f="$env:TEMP\Setup-SMTPGraphRelay.ps1";Invoke-WebRequest $u -UseBasicParsing -OutFile $f;& $f
|
$u='https://me-gitea.maieredv.cloud/MAIEREDV/SMTPGraphRelay/raw/branch/main/Setup-SMTPGraphRelay.ps1';$f="$env:TEMP\Setup-SMTPGraphRelay.ps1";Invoke-WebRequest $u -UseBasicParsing -OutFile $f;& $f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Neu in V1.7: SMTP AUTH
|
## Failed Queue verwalten
|
||||||
|
|
||||||
Unterstützt:
|
Im Manager:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
AUTH LOGIN
|
[10] Failed Queue verwalten
|
||||||
AUTH PLAIN
|
|
||||||
```
|
|
||||||
|
|
||||||
EHLO bewirbt die Mechanismen, sobald mindestens ein SMTP-Benutzer existiert:
|
|
||||||
|
|
||||||
```text
|
|
||||||
250-AUTH LOGIN PLAIN
|
|
||||||
```
|
|
||||||
|
|
||||||
Erfolgreiche Anmeldung:
|
|
||||||
|
|
||||||
```text
|
|
||||||
235 2.7.0 Authentication successful
|
|
||||||
```
|
|
||||||
|
|
||||||
Fehlgeschlagene Anmeldung:
|
|
||||||
|
|
||||||
```text
|
|
||||||
535 5.7.8 Authentication credentials invalid
|
|
||||||
```
|
|
||||||
|
|
||||||
Wenn AUTH für den Client erforderlich ist:
|
|
||||||
|
|
||||||
```text
|
|
||||||
530 5.7.0 Authentication required
|
|
||||||
```
|
|
||||||
|
|
||||||
Nach standardmäßig fünf Fehlversuchen innerhalb derselben Verbindung werden
|
|
||||||
weitere AUTH-Versuche temporär abgewiesen:
|
|
||||||
|
|
||||||
```text
|
|
||||||
454 4.7.0 Too many authentication failures
|
|
||||||
```
|
|
||||||
|
|
||||||
## IP-Allowlist bleibt vorgeschaltet
|
|
||||||
|
|
||||||
SMTP-AUTH ersetzt die bestehende `AllowedNetworks`-Prüfung **nicht**.
|
|
||||||
|
|
||||||
Ein Client muss zuerst aus einem erlaubten Netz kommen. Erst danach kann er sich
|
|
||||||
optional bzw. verpflichtend authentifizieren.
|
|
||||||
|
|
||||||
AUTH kann daher niemals einem ansonsten nicht erlaubten Host Zugriff verschaffen.
|
|
||||||
|
|
||||||
## Passwortspeicherung
|
|
||||||
|
|
||||||
SMTP-Passwörter werden nicht im Klartext gespeichert.
|
|
||||||
|
|
||||||
Verwendet wird:
|
|
||||||
|
|
||||||
```text
|
|
||||||
PBKDF2-HMAC-SHA256
|
|
||||||
Salt: 16 Byte zufällig
|
|
||||||
Iterations: 150000
|
|
||||||
Hash: 32 Byte
|
|
||||||
```
|
|
||||||
|
|
||||||
Beispiel in `config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Username": "scanner01",
|
|
||||||
"Salt": "...",
|
|
||||||
"PasswordHash": "...",
|
|
||||||
"Iterations": 150000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
```json
|
|
||||||
"Smtp": {
|
|
||||||
"RequireAuth": true,
|
|
||||||
"AuthMaxFailures": 5,
|
|
||||||
"AllowUnauthenticatedNetworks": [
|
|
||||||
"10.60.10.0/24"
|
|
||||||
],
|
|
||||||
"AuthUsers": [
|
|
||||||
{
|
|
||||||
"Username": "scanner01",
|
|
||||||
"Salt": "...",
|
|
||||||
"PasswordHash": "...",
|
|
||||||
"Iterations": 150000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### RequireAuth
|
|
||||||
|
|
||||||
```text
|
|
||||||
false
|
|
||||||
```
|
|
||||||
|
|
||||||
AUTH ist optional. Clients aus `AllowedNetworks` dürfen weiterhin ohne
|
|
||||||
Benutzername/Passwort senden.
|
|
||||||
|
|
||||||
```text
|
|
||||||
true
|
|
||||||
```
|
|
||||||
|
|
||||||
Clients müssen sich authentifizieren, außer ihre IP liegt in
|
|
||||||
`AllowUnauthenticatedNetworks`.
|
|
||||||
|
|
||||||
### AllowUnauthenticatedNetworks
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
"10.60.10.0/24",
|
|
||||||
"192.168.50.10"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Diese Hosts/Netze dürfen trotz `RequireAuth=true` ohne AUTH senden.
|
|
||||||
|
|
||||||
Sie müssen trotzdem zusätzlich durch `AllowedNetworks` erlaubt sein.
|
|
||||||
|
|
||||||
## Benutzerverwaltung
|
|
||||||
|
|
||||||
Im Setup-/Manager gibt es jetzt:
|
|
||||||
|
|
||||||
```text
|
|
||||||
[9] SMTP-AUTH verwalten
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Untermenü:
|
Untermenü:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
[1] SMTP-AUTH erforderlich EIN/AUS
|
[1] Failed Queue anzeigen
|
||||||
[2] Benutzer hinzufügen
|
[2] Details einer Mail anzeigen
|
||||||
[3] Benutzer anzeigen
|
[3] Eine Mail erneut zustellen
|
||||||
[4] Passwort ändern
|
[4] Alle Mails erneut zustellen
|
||||||
[5] Benutzer löschen
|
[5] Eine Mail endgültig löschen
|
||||||
[6] Netze ohne Auth verwalten
|
[6] Alle Failed-Mails endgültig löschen
|
||||||
[7] Max. Fehlversuche ändern
|
|
||||||
[0] Zurück
|
[0] Zurück
|
||||||
```
|
```
|
||||||
|
|
||||||
Beim Hinzufügen bzw. Ändern eines Passworts erfolgt die Eingabe verdeckt.
|
Die Übersicht zeigt u. a.:
|
||||||
|
|
||||||
## Graph-Sender bleibt unabhängig
|
|
||||||
|
|
||||||
Der SMTP-Benutzer dient ausschließlich zur Authentifizierung am lokalen Relay.
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SMTP AUTH: scanner01
|
|
||||||
MAIL FROM: scanner@device.local
|
|
||||||
|
|
|
||||||
v
|
|
||||||
SMTPGraphRelay
|
|
||||||
|
|
|
||||||
ForceSender=true
|
|
||||||
|
|
|
||||||
v
|
|
||||||
info@example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
AUTH verändert weder die Entra-App noch das Graph-Zertifikat oder
|
|
||||||
`Graph.SenderMailbox`.
|
|
||||||
|
|
||||||
## Queue-Metadaten
|
|
||||||
|
|
||||||
Bei authentifizierten Einlieferungen wird der Benutzer zusätzlich in den
|
|
||||||
Queue-Metadaten gespeichert:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"AuthenticatedUser": "scanner01"
|
|
||||||
```
|
|
||||||
|
|
||||||
und im Log ausgegeben:
|
|
||||||
|
|
||||||
```text
|
|
||||||
[7F3A91C2D441] Mail angenommen | ... | Auth=scanner01
|
|
||||||
```
|
|
||||||
|
|
||||||
Bei nicht authentifizierten, aber erlaubten Verbindungen:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Auth=unauthenticated
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Check
|
|
||||||
|
|
||||||
Der Health Check validiert zusätzlich die SMTP-AUTH-Konfiguration.
|
|
||||||
|
|
||||||
Eine Testmail kann weiterhin mit:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\Test-SMTPGraphRelay.ps1 -SendTestMail -TestRecipient "user@example.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
ausgeführt werden.
|
|
||||||
|
|
||||||
Ist AUTH erforderlich, fragt der Health Check bei Bedarf Benutzername und
|
|
||||||
Passwort interaktiv ab.
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\Test-SMTPGraphRelay.ps1 `
|
|
||||||
-SendTestMail `
|
|
||||||
-TestRecipient "user@example.com" `
|
|
||||||
-SmtpUsername "scanner01"
|
|
||||||
```
|
|
||||||
|
|
||||||
Das Passwort wird weiterhin verdeckt abgefragt.
|
|
||||||
|
|
||||||
## V1.6 Funktionen bleiben erhalten
|
|
||||||
|
|
||||||
- Queue-ID
|
- Queue-ID
|
||||||
- Received-Header
|
- Zeitpunkt
|
||||||
- automatische Message-ID
|
- RetryCount
|
||||||
- MaxRecipients
|
- Envelope-From
|
||||||
- MaxMessagesPerConnection
|
- Empfänger
|
||||||
- MaxPendingMessages
|
- letzten HTTP-/Graph-Status
|
||||||
- MinFreeDiskSpaceMB
|
- Größe
|
||||||
- Queue-/Disk-Backpressure
|
|
||||||
- parallele SMTP-Clients
|
In den Details steht zusätzlich die letzte Fehlermeldung und – falls vorhanden –
|
||||||
- Graph-Retry
|
der SMTP-AUTH-Benutzer.
|
||||||
- Log-Rotation
|
|
||||||
- Zertifikatsüberwachung
|
### Requeue
|
||||||
- Online-Update / Repair / Rollback
|
|
||||||
|
Beim erneuten Zustellen wird die `.eml` samt Metadaten von `failed` nach
|
||||||
|
`queue\pending` verschoben.
|
||||||
|
|
||||||
|
Dabei werden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RetryCount = 0
|
||||||
|
NextAttemptUtc = jetzt
|
||||||
|
RequeuedUtc = jetzt
|
||||||
|
```
|
||||||
|
|
||||||
|
gesetzt. Die alte Fehlerbeschreibung bleibt in den Metadaten erhalten, bis ein
|
||||||
|
neuer Versandversuch sie aktualisiert.
|
||||||
|
|
||||||
|
Der laufende Queue-Worker nimmt die Mail anschließend automatisch wieder auf.
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
Update und Repair verwenden nicht mehr zuerst `Stop-ScheduledTask`.
|
||||||
|
|
||||||
|
Stattdessen erstellt der Manager:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\Program Files\SMTPGraphRelay\shutdown.request
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Relay erkennt das Signal kurzfristig und führt diesen Ablauf aus:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Shutdown angefordert
|
||||||
|
|
|
||||||
|
v
|
||||||
|
SMTP-Listener schließen
|
||||||
|
|
|
||||||
|
+--> keine neuen Verbindungen mehr
|
||||||
|
|
|
||||||
|
v
|
||||||
|
laufende SMTP-Sessions auslaufen lassen
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Queue-/Graph-Worker beendet aktuellen Versand
|
||||||
|
|
|
||||||
|
+--> startet keine weitere Queue-Mail
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Runspaces schließen
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Graph-Verbindung trennen
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Relay beendet sich selbst
|
||||||
|
```
|
||||||
|
|
||||||
|
Standard-Timeout:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Smtp": {
|
||||||
|
"GracefulShutdownSeconds": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Zulässiger Bereich:
|
||||||
|
|
||||||
|
```text
|
||||||
|
5 bis 300 Sekunden
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn der Relay innerhalb dieses Zeitraums nicht selbst beendet ist, verwendet
|
||||||
|
der Manager als Fallback einen harten `Stop-ScheduledTask`.
|
||||||
|
|
||||||
|
Beim nächsten Start wird ein eventuell altes `shutdown.request` vorsorglich
|
||||||
|
entfernt.
|
||||||
|
|
||||||
|
## Warum das für Updates wichtig ist
|
||||||
|
|
||||||
|
Ein Update/Repair kann damit nicht mehr unnötig mitten in:
|
||||||
|
|
||||||
|
- einer SMTP-DATA-Übertragung
|
||||||
|
- einer aktiven SMTP-Session
|
||||||
|
- oder einem laufenden Graph-sendMail-Aufruf
|
||||||
|
|
||||||
|
den Prozess beenden.
|
||||||
|
|
||||||
|
Wenn ein Client oder Graph länger als das konfigurierte Grace-Timeout hängt,
|
||||||
|
wird nach Ablauf des Timeouts kontrolliert auf den bisherigen harten Stop
|
||||||
|
zurückgefallen.
|
||||||
|
|
||||||
|
## Manager
|
||||||
|
|
||||||
|
```text
|
||||||
|
[1] Neuinstallation aus Gitea
|
||||||
|
[2] Installation aus Gitea reparieren
|
||||||
|
[3] Nach Online-Updates suchen
|
||||||
|
[4] Entra / Exchange RBAC prüfen
|
||||||
|
[5] Zertifikat erneuern
|
||||||
|
[6] Health Check ausführen
|
||||||
|
[7] Deinstallieren
|
||||||
|
[8] Status anzeigen
|
||||||
|
[9] SMTP-AUTH verwalten
|
||||||
|
[10] Failed Queue verwalten
|
||||||
|
[0] Beenden
|
||||||
|
```
|
||||||
|
|||||||
+91
-9
@@ -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.7: SMTP AUTH LOGIN/PLAIN mit PBKDF2-SHA256, Benutzer-/Session-Schutz und V1.6 Queue-/Backpressure-Funktionen
|
V1.8: Failed-Queue-Verwaltung und kooperativer Graceful Shutdown; inklusive SMTP AUTH und V1.6 Queue-/Backpressure-Funktionen
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
@@ -697,6 +697,12 @@ function Process-Queue {
|
|||||||
$dirs = Get-QueueDirectories
|
$dirs = Get-QueueDirectories
|
||||||
|
|
||||||
foreach ($file in Get-ChildItem -LiteralPath $dirs.Pending -Filter "*.eml" -File | Sort-Object CreationTimeUtc) {
|
foreach ($file in Get-ChildItem -LiteralPath $dirs.Pending -Filter "*.eml" -File | Sort-Object CreationTimeUtc) {
|
||||||
|
if ($script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath" -and
|
||||||
|
(Test-Path -LiteralPath ([string]$script:Config.ShutdownSignalPath))) {
|
||||||
|
Write-Log "Queue-Worker: Shutdown angefordert; starte keine weitere Queue-Mail."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
$metaPath = "$($file.FullName).json"
|
$metaPath = "$($file.FullName).json"
|
||||||
|
|
||||||
if (Test-Path -LiteralPath $metaPath) {
|
if (Test-Path -LiteralPath $metaPath) {
|
||||||
@@ -1733,6 +1739,10 @@ function New-WorkerConfig {
|
|||||||
$copy.Paths.Failed = Resolve-PathFromConfig $script:Config.Paths.Failed
|
$copy.Paths.Failed = Resolve-PathFromConfig $script:Config.Paths.Failed
|
||||||
$copy.Paths.Logs = Resolve-PathFromConfig $script:Config.Paths.Logs
|
$copy.Paths.Logs = Resolve-PathFromConfig $script:Config.Paths.Logs
|
||||||
|
|
||||||
|
if ($script:ShutdownSignalPath) {
|
||||||
|
$copy | Add-Member -NotePropertyName ShutdownSignalPath -NotePropertyValue $script:ShutdownSignalPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
return $copy
|
return $copy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1827,6 +1837,22 @@ if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
|||||||
|
|
||||||
$script:Config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
$script:Config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
|
||||||
|
$script:InstallRoot = Split-Path -Parent ([IO.Path]::GetFullPath($ConfigPath))
|
||||||
|
$script:ShutdownSignalPath = Join-Path $script:InstallRoot "shutdown.request"
|
||||||
|
$script:GracefulShutdownSeconds = 30
|
||||||
|
|
||||||
|
if ($script:Config.Smtp.PSObject.Properties.Name -contains "GracefulShutdownSeconds") {
|
||||||
|
try {
|
||||||
|
$configuredGrace = [int]$script:Config.Smtp.GracefulShutdownSeconds
|
||||||
|
if ($configuredGrace -ge 5 -and $configuredGrace -le 300) {
|
||||||
|
$script:GracefulShutdownSeconds = $configuredGrace
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ein altes Signal darf einen frisch gestarteten Relay nicht sofort wieder beenden.
|
||||||
|
Remove-Item -LiteralPath $script:ShutdownSignalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -1946,6 +1972,12 @@ $queueBootstrap
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (`$true) {
|
while (`$true) {
|
||||||
|
if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath" -and
|
||||||
|
(Test-Path -LiteralPath ([string]`$script:Config.ShutdownSignalPath))) {
|
||||||
|
Write-Log "Queue-Worker: Shutdown-Signal erkannt."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Process-Queue
|
Process-Queue
|
||||||
}
|
}
|
||||||
@@ -1953,7 +1985,15 @@ try {
|
|||||||
Write-Log ("Queue-Worker: {0}" -f `$_.Exception.Message) "ERROR"
|
Write-Log ("Queue-Worker: {0}" -f `$_.Exception.Message) "ERROR"
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Sleep -Seconds ([int]`$script:Config.Queue.PollSeconds)
|
# In kleinen Schritten schlafen, damit Shutdown nicht erst nach PollSeconds reagiert.
|
||||||
|
`$sleepUntil = (Get-Date).AddSeconds([int]`$script:Config.Queue.PollSeconds)
|
||||||
|
while ((Get-Date) -lt `$sleepUntil) {
|
||||||
|
if (`$script:Config.PSObject.Properties.Name -contains "ShutdownSignalPath" -and
|
||||||
|
(Test-Path -LiteralPath ([string]`$script:Config.ShutdownSignalPath))) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -1973,7 +2013,7 @@ $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.7 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
|
Write-Log "SMTPGraphRelay V1.8 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."
|
||||||
@@ -1989,6 +2029,9 @@ $authMode = if ($authSettings.RequireAuth) { "erforderlich" } else { "optional/d
|
|||||||
Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f `
|
Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f `
|
||||||
$authMode, @($authSettings.Users).Count, $authSettings.AuthMaxFailures)
|
$authMode, @($authSettings.Users).Count, $authSettings.AuthMaxFailures)
|
||||||
|
|
||||||
|
Write-Log ("Graceful Shutdown: bis zu {0} Sekunden für aktive Sessions/Worker." -f `
|
||||||
|
$script:GracefulShutdownSeconds)
|
||||||
|
|
||||||
$logSettings = Get-LogSettings
|
$logSettings = Get-LogSettings
|
||||||
Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f `
|
Write-Log ("Log-Rotation: max. {0} MB pro Datei, Aufbewahrung {1} Tage." -f `
|
||||||
$logSettings.MaxFileSizeMB, $logSettings.RetentionDays)
|
$logSettings.MaxFileSizeMB, $logSettings.RetentionDays)
|
||||||
@@ -1997,14 +2040,24 @@ try {
|
|||||||
while ($true) {
|
while ($true) {
|
||||||
Remove-CompletedSmtpWorkers
|
Remove-CompletedSmtpWorkers
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $script:ShutdownSignalPath) {
|
||||||
|
Write-Log "Graceful Shutdown angefordert. SMTP-Listener wird geschlossen."
|
||||||
|
try { $listener.Stop() } catch {}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if ((Get-Date) -ge $script:NextCertificateCheck) {
|
if ((Get-Date) -ge $script:NextCertificateCheck) {
|
||||||
[void](Test-RelayCertificateExpiry)
|
[void](Test-RelayCertificateExpiry)
|
||||||
$script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours)
|
$script:NextCertificateCheck = (Get-Date).AddHours($script:CertificateCheckHours)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sollte der Queue-Worker unerwartet beendet werden, Relay nicht still
|
# Sollte der Queue-Worker unerwartet beendet werden, ist das nur dann ein
|
||||||
# ohne Versand weiterlaufen lassen.
|
# Fehler, wenn gerade KEIN geplanter Shutdown läuft.
|
||||||
if ($script:QueueAsyncResult.IsCompleted) {
|
if ($script:QueueAsyncResult.IsCompleted) {
|
||||||
|
if (Test-Path -LiteralPath $script:ShutdownSignalPath) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
|
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
|
||||||
throw "Queue-Worker wurde unerwartet beendet."
|
throw "Queue-Worker wurde unerwartet beendet."
|
||||||
@@ -2024,11 +2077,26 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Write-Log "SMTPGraphRelay wird beendet..." "INFO"
|
Write-Log "SMTPGraphRelay fährt kontrolliert herunter..."
|
||||||
|
|
||||||
try { $listener.Stop() } catch {}
|
try { $listener.Stop() } catch {}
|
||||||
|
|
||||||
# Neue Clients werden nicht mehr angenommen; bestehende Sessions schließen.
|
$deadline = (Get-Date).AddSeconds($script:GracefulShutdownSeconds)
|
||||||
|
|
||||||
|
# Laufende SMTP-Sessions bekommen Zeit, ihre aktuelle Nachricht/Session sauber
|
||||||
|
# abzuschließen. Es werden bereits keine neuen Verbindungen mehr angenommen.
|
||||||
|
while ($script:ActiveSmtpWorkers.Count -gt 0 -and (Get-Date) -lt $deadline) {
|
||||||
|
Remove-CompletedSmtpWorkers
|
||||||
|
|
||||||
|
if ($script:ActiveSmtpWorkers.Count -gt 0) {
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:ActiveSmtpWorkers.Count -gt 0) {
|
||||||
|
Write-Log ("Graceful-Shutdown-Timeout: {0} SMTP-Session(s) werden jetzt beendet." -f `
|
||||||
|
$script:ActiveSmtpWorkers.Count) "WARN"
|
||||||
|
|
||||||
foreach ($worker in @($script:ActiveSmtpWorkers)) {
|
foreach ($worker in @($script:ActiveSmtpWorkers)) {
|
||||||
try { $worker.Client.Close() } catch {}
|
try { $worker.Client.Close() } catch {}
|
||||||
try { $worker.PowerShell.Stop() } catch {}
|
try { $worker.PowerShell.Stop() } catch {}
|
||||||
@@ -2039,19 +2107,33 @@ finally {
|
|||||||
} catch {}
|
} catch {}
|
||||||
try { $worker.PowerShell.Dispose() } catch {}
|
try { $worker.PowerShell.Dispose() } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
$script:ActiveSmtpWorkers.Clear()
|
$script:ActiveSmtpWorkers.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
try { $script:SmtpRunspacePool.Close() } catch {}
|
try { $script:SmtpRunspacePool.Close() } catch {}
|
||||||
try { $script:SmtpRunspacePool.Dispose() } catch {}
|
try { $script:SmtpRunspacePool.Dispose() } catch {}
|
||||||
|
|
||||||
|
# Queue-Worker darf den aktuell laufenden Graph-Aufruf beenden. Durch das
|
||||||
|
# Shutdown-Signal startet Process-Queue anschließend keine weitere Mail.
|
||||||
|
while (-not $script:QueueAsyncResult.IsCompleted -and (Get-Date) -lt $deadline) {
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $script:QueueAsyncResult.IsCompleted) {
|
||||||
|
Write-Log "Graceful-Shutdown-Timeout: Queue-/Graph-Worker wird jetzt beendet." "WARN"
|
||||||
try { $script:QueuePowerShell.Stop() } catch {}
|
try { $script:QueuePowerShell.Stop() } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($script:QueueAsyncResult) {
|
if ($script:QueueAsyncResult) {
|
||||||
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
|
[void]$script:QueuePowerShell.EndInvoke($script:QueueAsyncResult)
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
try { $script:QueuePowerShell.Dispose() } catch {}
|
|
||||||
|
|
||||||
|
try { $script:QueuePowerShell.Dispose() } catch {}
|
||||||
try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {}
|
try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch {}
|
||||||
Write-Log "SMTPGraphRelay beendet."
|
|
||||||
|
Remove-Item -LiteralPath $script:ShutdownSignalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Log "SMTPGraphRelay sauber beendet."
|
||||||
}
|
}
|
||||||
|
|||||||
+356
-1
@@ -440,15 +440,60 @@ function Ensure-ScheduledTask {
|
|||||||
|
|
||||||
function Stop-RelayTask {
|
function Stop-RelayTask {
|
||||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
if ($task -and $task.State -eq "Running") {
|
|
||||||
|
if (-not $task -or $task.State -ne "Running") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$graceSeconds = 30
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = Get-RelayConfig -TargetPath $InstallPath
|
||||||
|
if ($config -and $config.Smtp.PSObject.Properties.Name -contains "GracefulShutdownSeconds") {
|
||||||
|
$configured = [int]$config.Smtp.GracefulShutdownSeconds
|
||||||
|
if ($configured -ge 5 -and $configured -le 300) {
|
||||||
|
$graceSeconds = $configured
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
|
||||||
|
$signalPath = Join-Path $InstallPath "shutdown.request"
|
||||||
|
|
||||||
|
try {
|
||||||
|
[IO.File]::WriteAllText(
|
||||||
|
$signalPath,
|
||||||
|
([DateTime]::UtcNow.ToString("o")),
|
||||||
|
(New-Object Text.UTF8Encoding($false))
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Info "Graceful Shutdown angefordert. Warte auf Relay (max. $graceSeconds Sekunden)..."
|
||||||
|
|
||||||
|
$deadline = (Get-Date).AddSeconds($graceSeconds + 5)
|
||||||
|
|
||||||
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
$current = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $current -or $current.State -ne "Running") {
|
||||||
|
Write-Ok "Relay sauber beendet."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warn "Graceful-Shutdown-Timeout erreicht. Task wird hart beendet."
|
||||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
Start-Sleep -Milliseconds 750
|
Start-Sleep -Milliseconds 750
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
Remove-Item -LiteralPath $signalPath -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Start-RelayTask {
|
function Start-RelayTask {
|
||||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
if ($task) {
|
if ($task) {
|
||||||
|
Remove-Item -LiteralPath (Join-Path $InstallPath "shutdown.request") -Force -ErrorAction SilentlyContinue
|
||||||
Start-ScheduledTask -TaskName $TaskName
|
Start-ScheduledTask -TaskName $TaskName
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
Write-Ok "Scheduled Task gestartet."
|
Write-Ok "Scheduled Task gestartet."
|
||||||
@@ -879,6 +924,7 @@ function Install-New {
|
|||||||
AuthMaxFailures = 5
|
AuthMaxFailures = 5
|
||||||
AllowUnauthenticatedNetworks = @()
|
AllowUnauthenticatedNetworks = @()
|
||||||
AuthUsers = @()
|
AuthUsers = @()
|
||||||
|
GracefulShutdownSeconds = 30
|
||||||
}
|
}
|
||||||
Graph = [ordered]@{
|
Graph = [ordered]@{
|
||||||
TenantId = $tenantId
|
TenantId = $tenantId
|
||||||
@@ -1483,6 +1529,308 @@ function Save-SmtpAuthConfigAndRestart {
|
|||||||
Start-RelayTask
|
Start-RelayTask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Get-FailedQueuePath {
|
||||||
|
param([Parameter(Mandatory)]$Config)
|
||||||
|
|
||||||
|
$path = [string]$Config.Paths.Failed
|
||||||
|
|
||||||
|
if ([IO.Path]::IsPathRooted($path)) {
|
||||||
|
return $path
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path $InstallPath $path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PendingQueuePath {
|
||||||
|
param([Parameter(Mandatory)]$Config)
|
||||||
|
|
||||||
|
$path = [string]$Config.Paths.Queue
|
||||||
|
|
||||||
|
if (-not [IO.Path]::IsPathRooted($path)) {
|
||||||
|
$path = Join-Path $InstallPath $path
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path $path "pending")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-FailedQueueEntries {
|
||||||
|
param([Parameter(Mandatory)]$Config)
|
||||||
|
|
||||||
|
$failedPath = Get-FailedQueuePath -Config $Config
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $failedPath)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = @()
|
||||||
|
|
||||||
|
foreach ($file in Get-ChildItem -LiteralPath $failedPath -Filter "*.eml" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime) {
|
||||||
|
$metaPath = "$($file.FullName).json"
|
||||||
|
$meta = $null
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $metaPath) {
|
||||||
|
try {
|
||||||
|
$meta = Get-Content -LiteralPath $metaPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$queueId = [IO.Path]::GetFileNameWithoutExtension($file.Name)
|
||||||
|
if ($meta -and $meta.PSObject.Properties.Name -contains "QueueId" -and $meta.QueueId) {
|
||||||
|
$queueId = [string]$meta.QueueId
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries += [pscustomobject]@{
|
||||||
|
QueueId = $queueId
|
||||||
|
FileName = $file.Name
|
||||||
|
Path = $file.FullName
|
||||||
|
MetaPath = $metaPath
|
||||||
|
SizeKB = [Math]::Round($file.Length / 1KB, 1)
|
||||||
|
FailedSince = $file.LastWriteTime
|
||||||
|
RetryCount = if ($meta -and $meta.RetryCount -ne $null) { [int]$meta.RetryCount } else { $null }
|
||||||
|
From = if ($meta) { [string]$meta.EnvelopeFrom } else { "" }
|
||||||
|
Recipients = if ($meta) { (@($meta.EnvelopeRecipients) -join ", ") } else { "" }
|
||||||
|
LastStatusCode = if ($meta) { [string]$meta.LastStatusCode } else { "" }
|
||||||
|
LastError = if ($meta) { [string]$meta.LastError } else { "" }
|
||||||
|
AuthenticatedUser = if ($meta -and $meta.PSObject.Properties.Name -contains "AuthenticatedUser") { [string]$meta.AuthenticatedUser } else { "" }
|
||||||
|
Meta = $meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Retry-FailedQueueEntry {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]$Config,
|
||||||
|
[Parameter(Mandatory)]$Entry
|
||||||
|
)
|
||||||
|
|
||||||
|
$pendingPath = Get-PendingQueuePath -Config $Config
|
||||||
|
New-Item -ItemType Directory -Path $pendingPath -Force | Out-Null
|
||||||
|
|
||||||
|
$targetEml = Join-Path $pendingPath $Entry.FileName
|
||||||
|
$targetMeta = "$targetEml.json"
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $targetEml) {
|
||||||
|
throw "Pending enthält bereits '$($Entry.FileName)'."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Entry.Meta) {
|
||||||
|
$meta = $Entry.Meta
|
||||||
|
|
||||||
|
if ($meta.PSObject.Properties.Name -contains "RetryCount") {
|
||||||
|
$meta.RetryCount = 0
|
||||||
|
} else {
|
||||||
|
$meta | Add-Member -NotePropertyName RetryCount -NotePropertyValue 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = [DateTime]::UtcNow.ToString("o")
|
||||||
|
|
||||||
|
if ($meta.PSObject.Properties.Name -contains "NextAttemptUtc") {
|
||||||
|
$meta.NextAttemptUtc = $now
|
||||||
|
} else {
|
||||||
|
$meta | Add-Member -NotePropertyName NextAttemptUtc -NotePropertyValue $now
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($meta.PSObject.Properties.Name -contains "RequeuedUtc") {
|
||||||
|
$meta.RequeuedUtc = $now
|
||||||
|
} else {
|
||||||
|
$meta | Add-Member -NotePropertyName RequeuedUtc -NotePropertyValue $now
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $Entry.MetaPath -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $Entry.MetaPath) {
|
||||||
|
Move-Item -LiteralPath $Entry.MetaPath -Destination $targetMeta -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Move-Item -LiteralPath $Entry.Path -Destination $targetEml -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-FailedQueueEntry {
|
||||||
|
param([Parameter(Mandatory)]$Entry)
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath $Entry.Path -Force -ErrorAction Stop
|
||||||
|
Remove-Item -LiteralPath $Entry.MetaPath -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
function Manage-FailedQueue {
|
||||||
|
$config = Get-RelayConfig -TargetPath $InstallPath
|
||||||
|
|
||||||
|
if (-not $config) {
|
||||||
|
Write-Fail "config.json nicht gefunden."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
Clear-Host
|
||||||
|
Write-Title "SMTPGraphRelay - Failed Queue"
|
||||||
|
|
||||||
|
$entries = @(Get-FailedQueueEntries -Config $config)
|
||||||
|
|
||||||
|
Write-Host "Failed-Mails: $($entries.Count)"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host " [1] Failed Queue anzeigen"
|
||||||
|
Write-Host " [2] Details einer Mail anzeigen"
|
||||||
|
Write-Host " [3] Eine Mail erneut zustellen"
|
||||||
|
Write-Host " [4] Alle Mails erneut zustellen"
|
||||||
|
Write-Host " [5] Eine Mail endgültig löschen"
|
||||||
|
Write-Host " [6] Alle Failed-Mails endgültig löschen"
|
||||||
|
Write-Host " [0] Zurück"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$choice = Read-Host "Auswahl"
|
||||||
|
|
||||||
|
switch ($choice) {
|
||||||
|
"1" {
|
||||||
|
if ($entries.Count -eq 0) {
|
||||||
|
Write-Ok "Failed Queue ist leer."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$entries |
|
||||||
|
Select-Object QueueId,FailedSince,RetryCount,From,Recipients,LastStatusCode,SizeKB |
|
||||||
|
Format-Table -AutoSize
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"2" {
|
||||||
|
$id = Read-Host "Queue-ID"
|
||||||
|
|
||||||
|
$entry = $entries |
|
||||||
|
Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $entry) {
|
||||||
|
Write-Fail "Queue-ID '$id' nicht gefunden."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Queue-ID: $($entry.QueueId)"
|
||||||
|
Write-Host "Datei: $($entry.FileName)"
|
||||||
|
Write-Host "Fehlgeschlagen:$($entry.FailedSince)"
|
||||||
|
Write-Host "RetryCount: $($entry.RetryCount)"
|
||||||
|
Write-Host "Von: $($entry.From)"
|
||||||
|
Write-Host "An: $($entry.Recipients)"
|
||||||
|
Write-Host "Auth-User: $($entry.AuthenticatedUser)"
|
||||||
|
Write-Host "Status: $($entry.LastStatusCode)"
|
||||||
|
Write-Host "Größe: $($entry.SizeKB) KB"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Letzter Fehler:" -ForegroundColor Yellow
|
||||||
|
Write-Host $entry.LastError
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"3" {
|
||||||
|
$id = Read-Host "Queue-ID"
|
||||||
|
|
||||||
|
$entry = $entries |
|
||||||
|
Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $entry) {
|
||||||
|
Write-Fail "Queue-ID '$id' nicht gefunden."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Retry-FailedQueueEntry -Config $config -Entry $entry
|
||||||
|
Write-Ok "[$($entry.QueueId)] zurück nach pending verschoben."
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"4" {
|
||||||
|
if ($entries.Count -eq 0) {
|
||||||
|
Write-Ok "Failed Queue ist leer."
|
||||||
|
}
|
||||||
|
elseif (Confirm-Yes "Alle $($entries.Count) Failed-Mails erneut zustellen?") {
|
||||||
|
$ok = 0
|
||||||
|
$failed = 0
|
||||||
|
|
||||||
|
foreach ($entry in $entries) {
|
||||||
|
try {
|
||||||
|
Retry-FailedQueueEntry -Config $config -Entry $entry
|
||||||
|
$ok++
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$failed++
|
||||||
|
Write-Warn "[$($entry.QueueId)] konnte nicht requeued werden: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Ok "$ok Mail(s) zurück nach pending verschoben."
|
||||||
|
if ($failed -gt 0) {
|
||||||
|
Write-Warn "$failed Mail(s) konnten nicht verschoben werden."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"5" {
|
||||||
|
$id = Read-Host "Queue-ID"
|
||||||
|
|
||||||
|
$entry = $entries |
|
||||||
|
Where-Object { $_.QueueId -eq $id -or $_.FileName -eq $id -or $_.FileName -eq "$id.eml" } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $entry) {
|
||||||
|
Write-Fail "Queue-ID '$id' nicht gefunden."
|
||||||
|
}
|
||||||
|
elseif (Confirm-Yes "[$($entry.QueueId)] endgültig aus Failed löschen?") {
|
||||||
|
Remove-FailedQueueEntry -Entry $entry
|
||||||
|
Write-Ok "[$($entry.QueueId)] gelöscht."
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"6" {
|
||||||
|
if ($entries.Count -eq 0) {
|
||||||
|
Write-Ok "Failed Queue ist leer."
|
||||||
|
}
|
||||||
|
elseif (Confirm-Yes "WIRKLICH alle $($entries.Count) Failed-Mails endgültig löschen?") {
|
||||||
|
if (Confirm-Yes "Endgültiges Löschen nochmals bestätigen?") {
|
||||||
|
$ok = 0
|
||||||
|
|
||||||
|
foreach ($entry in $entries) {
|
||||||
|
try {
|
||||||
|
Remove-FailedQueueEntry -Entry $entry
|
||||||
|
$ok++
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warn "[$($entry.QueueId)] konnte nicht gelöscht werden."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Ok "$ok Failed-Mail(s) endgültig gelöscht."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Read-Host "Enter"
|
||||||
|
}
|
||||||
|
|
||||||
|
"0" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default {
|
||||||
|
Write-Warn "Ungültige Auswahl."
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = Get-RelayConfig -TargetPath $InstallPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Manage-SmtpAuth {
|
function Manage-SmtpAuth {
|
||||||
$config = Get-RelayConfig -TargetPath $InstallPath
|
$config = Get-RelayConfig -TargetPath $InstallPath
|
||||||
|
|
||||||
@@ -1850,6 +2198,11 @@ function Show-Status {
|
|||||||
$authUsers = if ($config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { @($config.Smtp.AuthUsers).Count } else { 0 }
|
$authUsers = if ($config.Smtp.PSObject.Properties.Name -contains "AuthUsers") { @($config.Smtp.AuthUsers).Count } else { 0 }
|
||||||
Write-Host " AUTH: $authText ($authUsers Benutzer)"
|
Write-Host " AUTH: $authText ($authUsers Benutzer)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$failedCount = @(Get-FailedQueueEntries -Config $config).Count
|
||||||
|
Write-Host " Failed: $failedCount Mail(s)"
|
||||||
|
} catch {}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1887,6 +2240,7 @@ function Show-Menu {
|
|||||||
Write-Host " [7] Deinstallieren"
|
Write-Host " [7] Deinstallieren"
|
||||||
Write-Host " [8] Status anzeigen"
|
Write-Host " [8] Status anzeigen"
|
||||||
Write-Host " [9] SMTP-AUTH verwalten"
|
Write-Host " [9] SMTP-AUTH verwalten"
|
||||||
|
Write-Host " [10] Failed Queue verwalten"
|
||||||
Write-Host " [0] Beenden"
|
Write-Host " [0] Beenden"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
@@ -1908,6 +2262,7 @@ while ($true) {
|
|||||||
"7" { Uninstall-Relay }
|
"7" { Uninstall-Relay }
|
||||||
"8" { Show-Status }
|
"8" { Show-Status }
|
||||||
"9" { Manage-SmtpAuth }
|
"9" { Manage-SmtpAuth }
|
||||||
|
"10" { Manage-FailedQueue }
|
||||||
"0" { break }
|
"0" { break }
|
||||||
default { Write-Warn "Ungültige Auswahl." }
|
default { Write-Warn "Ungültige Auswahl." }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Version": "1.7.0",
|
"Version": "1.8.0",
|
||||||
"Product": "SMTPGraphRelay",
|
"Product": "SMTPGraphRelay",
|
||||||
"MinimumPowerShell": "5.1",
|
"MinimumPowerShell": "5.1",
|
||||||
"ReleaseDate": "2026-08-14",
|
"ReleaseDate": "2026-08-14",
|
||||||
|
|||||||
Reference in New Issue
Block a user