Dateien nach "/" hochladen
This commit is contained in:
+491
-18
@@ -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.6: Queue-ID/Received/Message-ID sowie SMTP-/Queue-Backpressure und Session-Limits
|
V1.7: SMTP AUTH LOGIN/PLAIN mit PBKDF2-SHA256, Benutzer-/Session-Schutz und V1.6 Queue-/Backpressure-Funktionen
|
||||||
#>
|
#>
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
@@ -809,6 +809,319 @@ function Add-RelayMessageHeaders {
|
|||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-AddressInList {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][System.Net.IPAddress]$Address,
|
||||||
|
[object[]]$Entries
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($entry in @($Entries)) {
|
||||||
|
if ($null -eq $entry) { continue }
|
||||||
|
|
||||||
|
$value = [string]$entry
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) { continue }
|
||||||
|
|
||||||
|
if ($value -eq "*") { return $true }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($value -match '/') {
|
||||||
|
if (Test-IPv4InCidr -Address $Address -Cidr $value) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ([System.Net.IPAddress]::Parse($value).Equals($Address)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SmtpAuthSettings {
|
||||||
|
$requireAuth = $false
|
||||||
|
$maxFailures = 5
|
||||||
|
$allowUnauthenticatedNetworks = @()
|
||||||
|
$users = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($script:Config.Smtp.PSObject.Properties.Name -contains "RequireAuth") {
|
||||||
|
$requireAuth = [bool]$script:Config.Smtp.RequireAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Config.Smtp.PSObject.Properties.Name -contains "AuthMaxFailures") {
|
||||||
|
$v = [int]$script:Config.Smtp.AuthMaxFailures
|
||||||
|
if ($v -ge 1 -and $v -le 100) {
|
||||||
|
$maxFailures = $v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Config.Smtp.PSObject.Properties.Name -contains "AllowUnauthenticatedNetworks") {
|
||||||
|
$allowUnauthenticatedNetworks = @($script:Config.Smtp.AllowUnauthenticatedNetworks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:Config.Smtp.PSObject.Properties.Name -contains "AuthUsers") {
|
||||||
|
$users = @($script:Config.Smtp.AuthUsers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
RequireAuth = $requireAuth
|
||||||
|
AuthMaxFailures = $maxFailures
|
||||||
|
AllowUnauthenticatedNetworks = $allowUnauthenticatedNetworks
|
||||||
|
Users = $users
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SmtpAuthenticationRequired {
|
||||||
|
param([Parameter(Mandatory)][System.Net.IPAddress]$Address)
|
||||||
|
|
||||||
|
$settings = Get-SmtpAuthSettings
|
||||||
|
|
||||||
|
if (-not $settings.RequireAuth) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-AddressInList -Address $Address -Entries $settings.AllowUnauthenticatedNetworks) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-Base64Utf8 {
|
||||||
|
param([Parameter(Mandatory)][string]$Value)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$bytes = [Convert]::FromBase64String($Value)
|
||||||
|
return [Text.Encoding]::UTF8.GetString($bytes)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Invalid Base64 data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Test-FixedTimeEquals {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][byte[]]$A,
|
||||||
|
[Parameter(Mandatory)][byte[]]$B
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($A.Length -ne $B.Length) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
[int]$difference = 0
|
||||||
|
for ($i = 0; $i -lt $A.Length; $i++) {
|
||||||
|
$difference = $difference -bor ($A[$i] -bxor $B[$i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($difference -eq 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SmtpCredentials {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$Username,
|
||||||
|
[Parameter(Mandatory)][string]$Password
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Username)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = Get-SmtpAuthSettings
|
||||||
|
$user = $settings.Users |
|
||||||
|
Where-Object { ([string]$_.Username).Equals($Username, [StringComparison]::OrdinalIgnoreCase) } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $user) {
|
||||||
|
# Bewusst keine Unterscheidung im SMTP-Result zwischen unbekanntem
|
||||||
|
# Benutzer und falschem Passwort.
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$salt = [Convert]::FromBase64String([string]$user.Salt)
|
||||||
|
$expected = [Convert]::FromBase64String([string]$user.PasswordHash)
|
||||||
|
$iterations = [int]$user.Iterations
|
||||||
|
|
||||||
|
if ($iterations -lt 10000 -or $expected.Length -lt 16 -or $salt.Length -lt 8) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = Invoke-Pbkdf2Sha256 `
|
||||||
|
-Password $Password `
|
||||||
|
-Salt $salt `
|
||||||
|
-Iterations $iterations `
|
||||||
|
-Length $expected.Length
|
||||||
|
|
||||||
|
return (Test-FixedTimeEquals -A $actual -B $expected)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SmtpAuthLogin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][System.IO.StreamReader]$Reader,
|
||||||
|
[Parameter(Mandatory)][System.IO.StreamWriter]$Writer,
|
||||||
|
[string]$InitialResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($InitialResponse)) {
|
||||||
|
Write-SmtpLine $Writer "334 VXNlcm5hbWU6"
|
||||||
|
$encodedUser = $Reader.ReadLine()
|
||||||
|
if ($null -eq $encodedUser) { throw "Client disconnected during AUTH LOGIN" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$encodedUser = $InitialResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = ConvertFrom-Base64Utf8 -Value $encodedUser
|
||||||
|
|
||||||
|
Write-SmtpLine $Writer "334 UGFzc3dvcmQ6"
|
||||||
|
$encodedPassword = $Reader.ReadLine()
|
||||||
|
if ($null -eq $encodedPassword) { throw "Client disconnected during AUTH LOGIN" }
|
||||||
|
|
||||||
|
$password = ConvertFrom-Base64Utf8 -Value $encodedPassword
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Valid = (Test-SmtpCredentials -Username $username -Password $password)
|
||||||
|
Username = $username
|
||||||
|
ProtocolError = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Valid = $false
|
||||||
|
Username = $null
|
||||||
|
ProtocolError = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SmtpAuthPlain {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][System.IO.StreamReader]$Reader,
|
||||||
|
[Parameter(Mandatory)][System.IO.StreamWriter]$Writer,
|
||||||
|
[string]$InitialResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$encoded = $InitialResponse
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($encoded)) {
|
||||||
|
Write-SmtpLine $Writer "334"
|
||||||
|
$encoded = $Reader.ReadLine()
|
||||||
|
if ($null -eq $encoded) { throw "Client disconnected during AUTH PLAIN" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = ConvertFrom-Base64Utf8 -Value $encoded
|
||||||
|
$parts = $decoded -split "`0", 3
|
||||||
|
|
||||||
|
if ($parts.Count -lt 3) {
|
||||||
|
throw "Invalid AUTH PLAIN payload"
|
||||||
|
}
|
||||||
|
|
||||||
|
# RFC 4616: [authzid] NUL authcid NUL passwd
|
||||||
|
$username = $parts[1]
|
||||||
|
$password = $parts[2]
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Valid = (Test-SmtpCredentials -Username $username -Password $password)
|
||||||
|
Username = $username
|
||||||
|
ProtocolError = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Valid = $false
|
||||||
|
Username = $null
|
||||||
|
ProtocolError = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-SmtpLimits {
|
function Get-SmtpLimits {
|
||||||
$maxRecipients = 50
|
$maxRecipients = 50
|
||||||
$maxMessagesPerConnection = 25
|
$maxMessagesPerConnection = 25
|
||||||
@@ -933,6 +1246,8 @@ function Save-SmtpMessage {
|
|||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$RemoteAddress,
|
[string]$RemoteAddress,
|
||||||
|
|
||||||
|
[string]$AuthenticatedUser,
|
||||||
|
|
||||||
[string]$QueueId
|
[string]$QueueId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -961,6 +1276,7 @@ function Save-SmtpMessage {
|
|||||||
QueueId = $QueueId
|
QueueId = $QueueId
|
||||||
ReceivedUtc = [DateTime]::UtcNow.ToString("o")
|
ReceivedUtc = [DateTime]::UtcNow.ToString("o")
|
||||||
RemoteAddress = $RemoteAddress
|
RemoteAddress = $RemoteAddress
|
||||||
|
AuthenticatedUser = $AuthenticatedUser
|
||||||
EnvelopeFrom = $MailFrom
|
EnvelopeFrom = $MailFrom
|
||||||
EnvelopeRecipients = @($Recipients)
|
EnvelopeRecipients = @($Recipients)
|
||||||
RetryCount = 0
|
RetryCount = 0
|
||||||
@@ -985,7 +1301,8 @@ function Save-SmtpMessage {
|
|||||||
throw
|
throw
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log "[$QueueId] Mail angenommen | Von=$MailFrom | An=$($Recipients -join ', ') | Client=$RemoteAddress"
|
$authText = if ([string]::IsNullOrWhiteSpace($AuthenticatedUser)) { "unauthenticated" } else { $AuthenticatedUser }
|
||||||
|
Write-Log "[$QueueId] Mail angenommen | Von=$MailFrom | An=$($Recipients -join ', ') | Client=$RemoteAddress | Auth=$authText"
|
||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
QueueId = $QueueId
|
QueueId = $QueueId
|
||||||
Path = $pendingEml
|
Path = $pendingEml
|
||||||
@@ -1016,6 +1333,7 @@ function Handle-SmtpClient {
|
|||||||
$writer.AutoFlush = $true
|
$writer.AutoFlush = $true
|
||||||
Write-SmtpLine $writer "554 5.7.1 Client not allowed"
|
Write-SmtpLine $writer "554 5.7.1 Client not allowed"
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
$Client.Close()
|
$Client.Close()
|
||||||
Write-Log "Client abgewiesen: $remoteIp" "WARN"
|
Write-Log "Client abgewiesen: $remoteIp" "WARN"
|
||||||
return
|
return
|
||||||
@@ -1023,8 +1341,22 @@ function Handle-SmtpClient {
|
|||||||
|
|
||||||
$stream = $Client.GetStream()
|
$stream = $Client.GetStream()
|
||||||
$stream.ReadTimeout = [int]$script:Config.Smtp.ClientTimeoutSeconds * 1000
|
$stream.ReadTimeout = [int]$script:Config.Smtp.ClientTimeoutSeconds * 1000
|
||||||
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8, $true, 4096, $true)
|
|
||||||
$writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII, 4096, $true)
|
$reader = New-Object System.IO.StreamReader(
|
||||||
|
$stream,
|
||||||
|
[System.Text.Encoding]::UTF8,
|
||||||
|
$true,
|
||||||
|
4096,
|
||||||
|
$true
|
||||||
|
)
|
||||||
|
|
||||||
|
$writer = New-Object System.IO.StreamWriter(
|
||||||
|
$stream,
|
||||||
|
[System.Text.Encoding]::ASCII,
|
||||||
|
4096,
|
||||||
|
$true
|
||||||
|
)
|
||||||
|
|
||||||
$writer.NewLine = "`r`n"
|
$writer.NewLine = "`r`n"
|
||||||
$writer.AutoFlush = $true
|
$writer.AutoFlush = $true
|
||||||
|
|
||||||
@@ -1032,6 +1364,14 @@ function Handle-SmtpClient {
|
|||||||
$recipients = New-Object System.Collections.Generic.List[string]
|
$recipients = New-Object System.Collections.Generic.List[string]
|
||||||
$acceptedMessages = 0
|
$acceptedMessages = 0
|
||||||
$limits = Get-SmtpLimits
|
$limits = Get-SmtpLimits
|
||||||
|
$authSettings = Get-SmtpAuthSettings
|
||||||
|
|
||||||
|
$authenticated = $false
|
||||||
|
$authenticatedUser = $null
|
||||||
|
$authFailures = 0
|
||||||
|
$authLocked = $false
|
||||||
|
$authRequired = Test-SmtpAuthenticationRequired -Address $remoteIp
|
||||||
|
$authAvailable = (@($authSettings.Users).Count -gt 0)
|
||||||
|
|
||||||
Write-SmtpLine $writer ("220 {0} SMTPGraphRelay ready" -f $script:Config.Smtp.Hostname)
|
Write-SmtpLine $writer ("220 {0} SMTPGraphRelay ready" -f $script:Config.Smtp.Hostname)
|
||||||
|
|
||||||
@@ -1040,12 +1380,112 @@ function Handle-SmtpClient {
|
|||||||
$line = $reader.ReadLine()
|
$line = $reader.ReadLine()
|
||||||
if ($null -eq $line) { break }
|
if ($null -eq $line) { break }
|
||||||
|
|
||||||
if ($line -match '^(?i)(EHLO|HELO)\s+(.+)$') {
|
if ($line -match '^(?i)EHLO\s+(.+)$') {
|
||||||
Write-SmtpLine $writer ("250-{0}" -f $script:Config.Smtp.Hostname)
|
Write-SmtpLine $writer ("250-{0}" -f $script:Config.Smtp.Hostname)
|
||||||
Write-SmtpLine $writer ("250-SIZE {0}" -f ([int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024))
|
Write-SmtpLine $writer ("250-SIZE {0}" -f ([int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024))
|
||||||
|
|
||||||
|
if ($authAvailable) {
|
||||||
|
Write-SmtpLine $writer "250-AUTH LOGIN PLAIN"
|
||||||
|
}
|
||||||
|
|
||||||
Write-SmtpLine $writer "250 8BITMIME"
|
Write-SmtpLine $writer "250 8BITMIME"
|
||||||
}
|
}
|
||||||
|
elseif ($line -match '^(?i)HELO\s+(.+)$') {
|
||||||
|
Write-SmtpLine $writer ("250 {0}" -f $script:Config.Smtp.Hostname)
|
||||||
|
}
|
||||||
|
elseif ($line -match '^(?i)AUTH\s+LOGIN(?:\s+(\S+))?\s*$') {
|
||||||
|
if (-not $authAvailable) {
|
||||||
|
Write-SmtpLine $writer "504 5.7.4 Authentication mechanism unavailable"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authenticated) {
|
||||||
|
Write-SmtpLine $writer "503 5.5.0 Already authenticated"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authLocked) {
|
||||||
|
Write-SmtpLine $writer "454 4.7.0 Too many authentication failures"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$initial = $matches[1]
|
||||||
|
$result = Invoke-SmtpAuthLogin -Reader $reader -Writer $writer -InitialResponse $initial
|
||||||
|
|
||||||
|
if ($result.ProtocolError) {
|
||||||
|
$authFailures++
|
||||||
|
Write-SmtpLine $writer "501 5.5.2 Invalid authentication data"
|
||||||
|
}
|
||||||
|
elseif ($result.Valid) {
|
||||||
|
$authenticated = $true
|
||||||
|
$authenticatedUser = $result.Username
|
||||||
|
$authFailures = 0
|
||||||
|
Write-SmtpLine $writer "235 2.7.0 Authentication successful"
|
||||||
|
Write-Log ("SMTP-AUTH LOGIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp, $authenticatedUser)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$authFailures++
|
||||||
|
Write-SmtpLine $writer "535 5.7.8 Authentication credentials invalid"
|
||||||
|
Write-Log ("SMTP-AUTH LOGIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp, $result.Username) "WARN"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authFailures -ge $authSettings.AuthMaxFailures) {
|
||||||
|
$authLocked = $true
|
||||||
|
Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures, $remoteIp) "WARN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($line -match '^(?i)AUTH\s+PLAIN(?:\s+(\S+))?\s*$') {
|
||||||
|
if (-not $authAvailable) {
|
||||||
|
Write-SmtpLine $writer "504 5.7.4 Authentication mechanism unavailable"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authenticated) {
|
||||||
|
Write-SmtpLine $writer "503 5.5.0 Already authenticated"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authLocked) {
|
||||||
|
Write-SmtpLine $writer "454 4.7.0 Too many authentication failures"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$initial = $matches[1]
|
||||||
|
$result = Invoke-SmtpAuthPlain -Reader $reader -Writer $writer -InitialResponse $initial
|
||||||
|
|
||||||
|
if ($result.ProtocolError) {
|
||||||
|
$authFailures++
|
||||||
|
Write-SmtpLine $writer "501 5.5.2 Invalid authentication data"
|
||||||
|
}
|
||||||
|
elseif ($result.Valid) {
|
||||||
|
$authenticated = $true
|
||||||
|
$authenticatedUser = $result.Username
|
||||||
|
$authFailures = 0
|
||||||
|
Write-SmtpLine $writer "235 2.7.0 Authentication successful"
|
||||||
|
Write-Log ("SMTP-AUTH PLAIN erfolgreich | Client={0} | Benutzer={1}" -f $remoteIp, $authenticatedUser)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$authFailures++
|
||||||
|
Write-SmtpLine $writer "535 5.7.8 Authentication credentials invalid"
|
||||||
|
Write-Log ("SMTP-AUTH PLAIN fehlgeschlagen | Client={0} | Benutzer={1}" -f $remoteIp, $result.Username) "WARN"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authFailures -ge $authSettings.AuthMaxFailures) {
|
||||||
|
$authLocked = $true
|
||||||
|
Write-Log ("SMTP-AUTH gesperrt nach {0} Fehlversuchen | Client={1}" -f $authFailures, $remoteIp) "WARN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($line -match '^(?i)AUTH\b') {
|
||||||
|
Write-SmtpLine $writer "504 5.5.4 Unsupported authentication mechanism"
|
||||||
|
}
|
||||||
elseif ($line -match '^(?i)MAIL FROM:\s*<([^>]*)>') {
|
elseif ($line -match '^(?i)MAIL FROM:\s*<([^>]*)>') {
|
||||||
|
if ($authRequired -and -not $authenticated) {
|
||||||
|
Write-SmtpLine $writer "530 5.7.0 Authentication required"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if ($acceptedMessages -ge $limits.MaxMessagesPerConnection) {
|
if ($acceptedMessages -ge $limits.MaxMessagesPerConnection) {
|
||||||
Write-SmtpLine $writer "452 4.5.3 Too many messages in this session"
|
Write-SmtpLine $writer "452 4.5.3 Too many messages in this session"
|
||||||
Write-Log ("SMTP-Session von {0}: Nachrichtenlimit {1} erreicht." -f $remoteIp, $limits.MaxMessagesPerConnection) "WARN"
|
Write-Log ("SMTP-Session von {0}: Nachrichtenlimit {1} erreicht." -f $remoteIp, $limits.MaxMessagesPerConnection) "WARN"
|
||||||
@@ -1072,6 +1512,11 @@ function Handle-SmtpClient {
|
|||||||
Write-SmtpLine $writer "250 2.1.5 OK"
|
Write-SmtpLine $writer "250 2.1.5 OK"
|
||||||
}
|
}
|
||||||
elseif ($line -match '^(?i)DATA\s*$') {
|
elseif ($line -match '^(?i)DATA\s*$') {
|
||||||
|
if ($authRequired -and -not $authenticated) {
|
||||||
|
Write-SmtpLine $writer "530 5.7.0 Authentication required"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (-not $mailFrom -or $recipients.Count -eq 0) {
|
if (-not $mailFrom -or $recipients.Count -eq 0) {
|
||||||
Write-SmtpLine $writer "503 5.5.1 Need MAIL FROM and RCPT TO first"
|
Write-SmtpLine $writer "503 5.5.1 Need MAIL FROM and RCPT TO first"
|
||||||
continue
|
continue
|
||||||
@@ -1082,8 +1527,6 @@ function Handle-SmtpClient {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
# Backpressure VOR 354/DATA: Der Client soll große Nachrichtendaten
|
|
||||||
# gar nicht erst übertragen, wenn wir sie nicht sicher puffern können.
|
|
||||||
$pressure = Get-QueuePressure
|
$pressure = Get-QueuePressure
|
||||||
if (-not $pressure.Accept) {
|
if (-not $pressure.Accept) {
|
||||||
Write-SmtpLine $writer $pressure.SmtpCode
|
Write-SmtpLine $writer $pressure.SmtpCode
|
||||||
@@ -1092,6 +1535,7 @@ function Handle-SmtpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-SmtpLine $writer "354 End data with <CR><LF>.<CR><LF>"
|
Write-SmtpLine $writer "354 End data with <CR><LF>.<CR><LF>"
|
||||||
|
|
||||||
$data = New-Object System.Collections.Generic.List[string]
|
$data = New-Object System.Collections.Generic.List[string]
|
||||||
$size = 0
|
$size = 0
|
||||||
$maxBytes = [int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024
|
$maxBytes = [int64]$script:Config.Smtp.MaxMessageSizeMB * 1024 * 1024
|
||||||
@@ -1099,13 +1543,21 @@ function Handle-SmtpClient {
|
|||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
$dataLine = $reader.ReadLine()
|
$dataLine = $reader.ReadLine()
|
||||||
if ($null -eq $dataLine) { throw "Client disconnected during DATA" }
|
|
||||||
if ($dataLine -eq ".") { break }
|
|
||||||
|
|
||||||
# SMTP dot-stuffing rückgängig machen
|
if ($null -eq $dataLine) {
|
||||||
if ($dataLine.StartsWith("..")) { $dataLine = $dataLine.Substring(1) }
|
throw "Client disconnected during DATA"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dataLine -eq ".") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dataLine.StartsWith("..")) {
|
||||||
|
$dataLine = $dataLine.Substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
$size += [System.Text.Encoding]::UTF8.GetByteCount($dataLine) + 2
|
$size += [System.Text.Encoding]::UTF8.GetByteCount($dataLine) + 2
|
||||||
|
|
||||||
if ($size -gt $maxBytes) {
|
if ($size -gt $maxBytes) {
|
||||||
$tooLarge = $true
|
$tooLarge = $true
|
||||||
}
|
}
|
||||||
@@ -1127,17 +1579,24 @@ function Handle-SmtpClient {
|
|||||||
-MailFrom $mailFrom `
|
-MailFrom $mailFrom `
|
||||||
-Recipients $recipients.ToArray() `
|
-Recipients $recipients.ToArray() `
|
||||||
-RemoteAddress $remoteIp.ToString() `
|
-RemoteAddress $remoteIp.ToString() `
|
||||||
|
-AuthenticatedUser $authenticatedUser `
|
||||||
-QueueId $queueId
|
-QueueId $queueId
|
||||||
|
|
||||||
$acceptedMessages++
|
$acceptedMessages++
|
||||||
Write-SmtpLine $writer ("250 2.0.0 Message accepted for delivery; queue-id={0}" -f $saved.QueueId)
|
|
||||||
|
Write-SmtpLine $writer (
|
||||||
|
"250 2.0.0 Message accepted for delivery; queue-id={0}" -f $saved.QueueId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# Nach DATA darf niemals 250 gesendet werden, wenn die Queue-Datei
|
|
||||||
# nicht vollständig und atomar gesichert werden konnte.
|
|
||||||
Write-SmtpLine $writer "451 4.3.0 SMTPGraphRelay queue temporarily unavailable"
|
Write-SmtpLine $writer "451 4.3.0 SMTPGraphRelay queue temporarily unavailable"
|
||||||
Write-Log ("[{0}] Queue-Speicherung fehlgeschlagen | Client={1} | Fehler={2}" -f `
|
|
||||||
$queueId, $remoteIp, $_.Exception.Message) "ERROR"
|
Write-Log (
|
||||||
|
"[{0}] Queue-Speicherung fehlgeschlagen | Client={1} | Fehler={2}" -f `
|
||||||
|
$queueId,
|
||||||
|
$remoteIp,
|
||||||
|
$_.Exception.Message
|
||||||
|
) "ERROR"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,7 +1615,7 @@ function Handle-SmtpClient {
|
|||||||
Write-SmtpLine $writer "221 2.0.0 Bye"
|
Write-SmtpLine $writer "221 2.0.0 Bye"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
elseif ($line -match '^(?i)(AUTH|STARTTLS)\b') {
|
elseif ($line -match '^(?i)STARTTLS\b') {
|
||||||
Write-SmtpLine $writer "502 5.5.1 Command not implemented"
|
Write-SmtpLine $writer "502 5.5.1 Command not implemented"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1424,6 +1883,15 @@ $smtpFunctionNames = @(
|
|||||||
"Get-QueueDirectories",
|
"Get-QueueDirectories",
|
||||||
"New-QueueId",
|
"New-QueueId",
|
||||||
"Add-RelayMessageHeaders",
|
"Add-RelayMessageHeaders",
|
||||||
|
"Test-AddressInList",
|
||||||
|
"Get-SmtpAuthSettings",
|
||||||
|
"Test-SmtpAuthenticationRequired",
|
||||||
|
"ConvertFrom-Base64Utf8",
|
||||||
|
"Invoke-Pbkdf2Sha256",
|
||||||
|
"Test-FixedTimeEquals",
|
||||||
|
"Test-SmtpCredentials",
|
||||||
|
"Invoke-SmtpAuthLogin",
|
||||||
|
"Invoke-SmtpAuthPlain",
|
||||||
"Get-SmtpLimits",
|
"Get-SmtpLimits",
|
||||||
"Get-QueuePressure",
|
"Get-QueuePressure",
|
||||||
"Save-SmtpMessage",
|
"Save-SmtpMessage",
|
||||||
@@ -1505,7 +1973,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.6 gestartet auf $($script:Config.Smtp.ListenAddress):$($script:Config.Smtp.Port)"
|
Write-Log "SMTPGraphRelay V1.7 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."
|
||||||
@@ -1516,6 +1984,11 @@ Write-Log ("SMTP-Limits: max. {0} Empfänger/Mail, {1} Mails/Verbindung." -f `
|
|||||||
Write-Log ("Backpressure: max. {0} Pending-Mails, mindestens {1} MB freier Speicher." -f `
|
Write-Log ("Backpressure: max. {0} Pending-Mails, mindestens {1} MB freier Speicher." -f `
|
||||||
$limits.MaxPendingMessages, $limits.MinFreeDiskSpaceMB)
|
$limits.MaxPendingMessages, $limits.MinFreeDiskSpaceMB)
|
||||||
|
|
||||||
|
$authSettings = Get-SmtpAuthSettings
|
||||||
|
$authMode = if ($authSettings.RequireAuth) { "erforderlich" } else { "optional/deaktiviert" }
|
||||||
|
Write-Log ("SMTP-AUTH: {0}; {1} Benutzer; max. {2} Fehlversuche/Verbindung." -f `
|
||||||
|
$authMode, @($authSettings.Users).Count, $authSettings.AuthMaxFailures)
|
||||||
|
|
||||||
$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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,674 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Health Check für SMTPGraphRelay.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Prüft die lokale SMTPGraphRelay-Installation ohne Änderungen vorzunehmen.
|
||||||
|
|
||||||
|
Standardprüfungen:
|
||||||
|
- Windows PowerShell 5.1
|
||||||
|
- Administratorstatus
|
||||||
|
- config.json
|
||||||
|
- Microsoft.Graph.Authentication Modul
|
||||||
|
- Zertifikat + Private Key + Ablaufdatum
|
||||||
|
- Verzeichnisse und Schreibrechte
|
||||||
|
- Scheduled Task
|
||||||
|
- SMTP Listener
|
||||||
|
- Queue / Failed Queue
|
||||||
|
- App-only Microsoft Graph Anmeldung
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
- echte SMTP-Testmail über das lokale Relay
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Test-SMTPGraphRelay.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Test-SMTPGraphRelay.ps1 -SendTestMail -TestRecipient manuel.maier@maieredv.de
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ConfigPath = "$PSScriptRoot\config.json",
|
||||||
|
|
||||||
|
[switch]$SendTestMail,
|
||||||
|
|
||||||
|
[string]$TestRecipient,
|
||||||
|
|
||||||
|
[string]$SmtpUsername,
|
||||||
|
|
||||||
|
[int]$QueueWarningAgeMinutes = 30,
|
||||||
|
|
||||||
|
[int]$FailedWarningCount = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
$script:OkCount = 0
|
||||||
|
$script:WarnCount = 0
|
||||||
|
$script:FailCount = 0
|
||||||
|
|
||||||
|
function Write-Result {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidateSet("OK","WARN","FAIL","INFO")]
|
||||||
|
[string]$Status,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Message
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($Status) {
|
||||||
|
"OK" {
|
||||||
|
$script:OkCount++
|
||||||
|
Write-Host "[OK] $Message" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
"WARN" {
|
||||||
|
$script:WarnCount++
|
||||||
|
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
"FAIL" {
|
||||||
|
$script:FailCount++
|
||||||
|
Write-Host "[FAIL] $Message" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
"INFO" {
|
||||||
|
Write-Host "[INFO] $Message" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ConfigPath {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
|
||||||
|
if ([IO.Path]::IsPathRooted($Path)) {
|
||||||
|
return $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return Join-Path $PSScriptRoot $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-IsAdministrator {
|
||||||
|
try {
|
||||||
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||||
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DirectoryWritable {
|
||||||
|
param([Parameter(Mandatory)][string]$Path)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$testFile = Join-Path $Path (".healthcheck-{0}.tmp" -f [guid]::NewGuid().ToString("N"))
|
||||||
|
[IO.File]::WriteAllText($testFile, "SMTPGraphRelay health check")
|
||||||
|
Remove-Item -LiteralPath $testFile -Force
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-RawSmtpTestMail {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$Server,
|
||||||
|
[Parameter(Mandatory)][int]$Port,
|
||||||
|
[Parameter(Mandatory)][string]$From,
|
||||||
|
[Parameter(Mandatory)][string]$To,
|
||||||
|
[string]$Username,
|
||||||
|
[string]$Password
|
||||||
|
)
|
||||||
|
|
||||||
|
$client = New-Object System.Net.Sockets.TcpClient
|
||||||
|
|
||||||
|
try {
|
||||||
|
$connect = $client.BeginConnect($Server, $Port, $null, $null)
|
||||||
|
|
||||||
|
if (-not $connect.AsyncWaitHandle.WaitOne(5000)) {
|
||||||
|
throw "Timeout beim Verbindungsaufbau zu $Server`:$Port"
|
||||||
|
}
|
||||||
|
|
||||||
|
$client.EndConnect($connect)
|
||||||
|
|
||||||
|
$stream = $client.GetStream()
|
||||||
|
$stream.ReadTimeout = 5000
|
||||||
|
$stream.WriteTimeout = 5000
|
||||||
|
|
||||||
|
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::ASCII)
|
||||||
|
$writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII)
|
||||||
|
$writer.NewLine = "`r`n"
|
||||||
|
$writer.AutoFlush = $true
|
||||||
|
|
||||||
|
function Read-SmtpResponse {
|
||||||
|
param([int[]]$ExpectedCodes)
|
||||||
|
|
||||||
|
$lines = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$line = $reader.ReadLine()
|
||||||
|
|
||||||
|
if ($null -eq $line) {
|
||||||
|
throw "SMTP-Verbindung unerwartet geschlossen."
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines.Add($line)
|
||||||
|
|
||||||
|
if ($line -match '^(\d{3})([ -])') {
|
||||||
|
$code = [int]$matches[1]
|
||||||
|
$separator = $matches[2]
|
||||||
|
|
||||||
|
if ($separator -eq " ") {
|
||||||
|
if ($ExpectedCodes -notcontains $code) {
|
||||||
|
throw "Unerwartete SMTP-Antwort: $($lines -join ' | ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($lines -join " | ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(220))
|
||||||
|
|
||||||
|
$writer.WriteLine("EHLO localhost")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(250))
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($Username)) {
|
||||||
|
$writer.WriteLine("AUTH LOGIN")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(334))
|
||||||
|
|
||||||
|
$writer.WriteLine([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Username)))
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(334))
|
||||||
|
|
||||||
|
$writer.WriteLine([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Password)))
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(235))
|
||||||
|
}
|
||||||
|
|
||||||
|
$writer.WriteLine("MAIL FROM:<$From>")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(250))
|
||||||
|
|
||||||
|
$writer.WriteLine("RCPT TO:<$To>")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(250))
|
||||||
|
|
||||||
|
$writer.WriteLine("DATA")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(354))
|
||||||
|
|
||||||
|
$subject = "SMTPGraphRelay Health Check $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||||
|
|
||||||
|
$writer.WriteLine("From: <$From>")
|
||||||
|
$writer.WriteLine("To: <$To>")
|
||||||
|
$writer.WriteLine("Subject: $subject")
|
||||||
|
$writer.WriteLine("Date: $([DateTime]::Now.ToString('ddd, dd MMM yyyy HH:mm:ss zzz', [Globalization.CultureInfo]::InvariantCulture))")
|
||||||
|
$writer.WriteLine("Message-ID: <$([guid]::NewGuid().ToString('N'))@smtpgraphrelay-healthcheck>")
|
||||||
|
$writer.WriteLine("MIME-Version: 1.0")
|
||||||
|
$writer.WriteLine("Content-Type: text/plain; charset=utf-8")
|
||||||
|
$writer.WriteLine("")
|
||||||
|
$writer.WriteLine("SMTPGraphRelay Health Check")
|
||||||
|
$writer.WriteLine("")
|
||||||
|
$writer.WriteLine("Zeit: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
|
||||||
|
$writer.WriteLine("Host: $env:COMPUTERNAME")
|
||||||
|
$writer.WriteLine(".")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(250))
|
||||||
|
|
||||||
|
$writer.WriteLine("QUIT")
|
||||||
|
[void](Read-SmtpResponse -ExpectedCodes @(221))
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
try { $client.Close() } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " SMTPGraphRelay - Health Check" -ForegroundColor Cyan
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PowerShell / Rechte
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ($PSVersionTable.PSEdition -eq "Desktop" -and $PSVersionTable.PSVersion.Major -eq 5) {
|
||||||
|
Write-Result OK "Windows PowerShell $($PSVersionTable.PSVersion) erkannt."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Dieses Tool sollte mit Windows PowerShell 5.1 laufen. Erkannt: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-IsAdministrator) {
|
||||||
|
Write-Result OK "PowerShell läuft als Administrator."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "PowerShell läuft nicht als Administrator. Einige Prüfungen können eingeschränkt sein."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
||||||
|
Write-Result FAIL "config.json nicht gefunden: $ConfigPath"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Health Check abgebrochen, da ohne Config keine weiteren Prüfungen möglich sind." -ForegroundColor Red
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
Write-Result OK "config.json konnte gelesen und geparst werden."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Result FAIL "config.json ist ungültig: $($_.Exception.Message)"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredConfig = @(
|
||||||
|
"Smtp.ListenAddress",
|
||||||
|
"Smtp.Port",
|
||||||
|
"Graph.TenantId",
|
||||||
|
"Graph.ClientId",
|
||||||
|
"Graph.CertificateThumbprint",
|
||||||
|
"Graph.SenderMailbox",
|
||||||
|
"Paths.Queue",
|
||||||
|
"Paths.Failed",
|
||||||
|
"Paths.Logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
$configMissing = $false
|
||||||
|
|
||||||
|
foreach ($item in $requiredConfig) {
|
||||||
|
$parts = $item -split '\.'
|
||||||
|
$value = $config
|
||||||
|
|
||||||
|
foreach ($part in $parts) {
|
||||||
|
if ($null -eq $value -or -not ($value.PSObject.Properties.Name -contains $part)) {
|
||||||
|
$value = $null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $value.$part
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $value -or ([string]$value).Trim().Length -eq 0) {
|
||||||
|
Write-Result FAIL "Config-Wert fehlt: $item"
|
||||||
|
$configMissing = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $configMissing) {
|
||||||
|
Write-Result OK "Alle erforderlichen Config-Werte sind vorhanden."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SMTP-AUTH Config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$authRequired = $false
|
||||||
|
$authUsers = @()
|
||||||
|
|
||||||
|
if ($config.Smtp.PSObject.Properties.Name -contains "RequireAuth") {
|
||||||
|
$authRequired = [bool]$config.Smtp.RequireAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config.Smtp.PSObject.Properties.Name -contains "AuthUsers") {
|
||||||
|
$authUsers = @($config.Smtp.AuthUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authRequired -and $authUsers.Count -eq 0) {
|
||||||
|
Write-Result FAIL "SMTP-AUTH ist erforderlich, aber es sind keine AuthUsers konfiguriert."
|
||||||
|
}
|
||||||
|
elseif ($authRequired) {
|
||||||
|
Write-Result OK "SMTP-AUTH ist erforderlich; $($authUsers.Count) Benutzer konfiguriert."
|
||||||
|
}
|
||||||
|
elseif ($authUsers.Count -gt 0) {
|
||||||
|
Write-Result OK "SMTP-AUTH ist optional; $($authUsers.Count) Benutzer konfiguriert."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result INFO "SMTP-AUTH ist nicht erforderlich und es sind keine Benutzer konfiguriert."
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($authUser in $authUsers) {
|
||||||
|
$valid = $true
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$authUser.Username)) { $valid = $false }
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$authUser.Salt)) { $valid = $false }
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$authUser.PasswordHash)) { $valid = $false }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ([int]$authUser.Iterations -lt 10000) { $valid = $false }
|
||||||
|
[void][Convert]::FromBase64String([string]$authUser.Salt)
|
||||||
|
[void][Convert]::FromBase64String([string]$authUser.PasswordHash)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$valid = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $valid) {
|
||||||
|
Write-Result FAIL "SMTP-AUTH Benutzer '$($authUser.Username)' besitzt ungültige Hash-/Salt-Daten."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Module
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$graphModule = Get-Module -ListAvailable -Name Microsoft.Graph.Authentication |
|
||||||
|
Sort-Object Version -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($graphModule) {
|
||||||
|
Write-Result OK "Microsoft.Graph.Authentication $($graphModule.Version) gefunden: $($graphModule.ModuleBase)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Microsoft.Graph.Authentication ist nicht installiert."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Zertifikat
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$cert = $null
|
||||||
|
$certPath = "Cert:\LocalMachine\My\$($config.Graph.CertificateThumbprint)"
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cert = Get-Item -LiteralPath $certPath -ErrorAction Stop
|
||||||
|
Write-Result OK "Relay-Zertifikat gefunden: $($cert.Thumbprint)"
|
||||||
|
|
||||||
|
if ($cert.HasPrivateKey) {
|
||||||
|
Write-Result OK "Zertifikat besitzt einen privaten Schlüssel."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Zertifikat besitzt keinen privaten Schlüssel."
|
||||||
|
}
|
||||||
|
|
||||||
|
$daysRemaining = [Math]::Floor(($cert.NotAfter.ToUniversalTime() - [DateTime]::UtcNow).TotalDays)
|
||||||
|
|
||||||
|
if ($daysRemaining -lt 0) {
|
||||||
|
Write-Result FAIL "Zertifikat ist abgelaufen seit $($cert.NotAfter)."
|
||||||
|
}
|
||||||
|
elseif ($daysRemaining -le 14) {
|
||||||
|
Write-Result FAIL "Zertifikat läuft in $daysRemaining Tagen ab ($($cert.NotAfter))."
|
||||||
|
}
|
||||||
|
elseif ($daysRemaining -le 60) {
|
||||||
|
Write-Result WARN "Zertifikat läuft in $daysRemaining Tagen ab ($($cert.NotAfter))."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result OK "Zertifikat gültig bis $($cert.NotAfter) ($daysRemaining Tage verbleibend)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Result FAIL "Relay-Zertifikat nicht gefunden oder nicht lesbar: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pfade / Queue
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$queueRoot = Resolve-ConfigPath $config.Paths.Queue
|
||||||
|
$failedDir = Resolve-ConfigPath $config.Paths.Failed
|
||||||
|
$logsDir = Resolve-ConfigPath $config.Paths.Logs
|
||||||
|
|
||||||
|
$queueIncoming = Join-Path $queueRoot "incoming"
|
||||||
|
$queuePending = Join-Path $queueRoot "pending"
|
||||||
|
$queueProcessing = Join-Path $queueRoot "processing"
|
||||||
|
|
||||||
|
foreach ($entry in @(
|
||||||
|
@{ Name = "Queue Root"; Path = $queueRoot },
|
||||||
|
@{ Name = "Queue incoming"; Path = $queueIncoming },
|
||||||
|
@{ Name = "Queue pending"; Path = $queuePending },
|
||||||
|
@{ Name = "Queue processing"; Path = $queueProcessing },
|
||||||
|
@{ Name = "Failed"; Path = $failedDir },
|
||||||
|
@{ Name = "Logs"; Path = $logsDir }
|
||||||
|
)) {
|
||||||
|
if (Test-Path -LiteralPath $entry.Path) {
|
||||||
|
if (Test-DirectoryWritable -Path $entry.Path) {
|
||||||
|
Write-Result OK "$($entry.Name) vorhanden und beschreibbar: $($entry.Path)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "$($entry.Name) vorhanden, aber nicht beschreibbar: $($entry.Path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "$($entry.Name) fehlt: $($entry.Path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pendingFiles = @()
|
||||||
|
$processingFiles = @()
|
||||||
|
$failedFiles = @()
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $queuePending) {
|
||||||
|
$pendingFiles = @(Get-ChildItem -LiteralPath $queuePending -Filter "*.eml" -File -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $queueProcessing) {
|
||||||
|
$processingFiles = @(Get-ChildItem -LiteralPath $queueProcessing -Filter "*.eml" -File -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $failedDir) {
|
||||||
|
$failedFiles = @(Get-ChildItem -LiteralPath $failedDir -Filter "*.eml" -File -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pendingFiles.Count -eq 0) {
|
||||||
|
Write-Result OK "Pending Queue ist leer."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$oldestPending = $pendingFiles | Sort-Object LastWriteTime | Select-Object -First 1
|
||||||
|
$ageMinutes = [Math]::Floor(((Get-Date) - $oldestPending.LastWriteTime).TotalMinutes)
|
||||||
|
|
||||||
|
if ($ageMinutes -ge $QueueWarningAgeMinutes) {
|
||||||
|
Write-Result WARN "$($pendingFiles.Count) Mail(s) in pending; älteste ist $ageMinutes Minuten alt."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result INFO "$($pendingFiles.Count) Mail(s) in pending; älteste ist $ageMinutes Minuten alt."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($processingFiles.Count -gt 0) {
|
||||||
|
Write-Result WARN "$($processingFiles.Count) Mail(s) liegen aktuell in processing."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result OK "Processing Queue ist leer."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failedFiles.Count -ge $FailedWarningCount) {
|
||||||
|
Write-Result WARN "$($failedFiles.Count) Mail(s) liegen in failed."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result OK "Failed Queue enthält $($failedFiles.Count) Mail(s)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scheduled Task
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$task = Get-ScheduledTask -TaskName "SMTPGraphRelay" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($task) {
|
||||||
|
Write-Result OK "Scheduled Task 'SMTPGraphRelay' vorhanden."
|
||||||
|
|
||||||
|
$taskInfo = Get-ScheduledTaskInfo -TaskName "SMTPGraphRelay"
|
||||||
|
|
||||||
|
if ($task.State -eq "Running") {
|
||||||
|
Write-Result OK "Scheduled Task läuft."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "Scheduled Task State: $($task.State)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($taskInfo.LastTaskResult -eq 0 -or $taskInfo.LastTaskResult -eq 267009) {
|
||||||
|
Write-Result OK "LastTaskResult: $($taskInfo.LastTaskResult)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "LastTaskResult: $($taskInfo.LastTaskResult)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $task.Actions | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($action.Execute -match 'WindowsPowerShell\\v1\.0\\powershell\.exe$') {
|
||||||
|
Write-Result OK "Scheduled Task verwendet Windows PowerShell 5.1."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "Scheduled Task verwendet unerwartetes PowerShell-Binary: $($action.Execute)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Scheduled Task 'SMTPGraphRelay' wurde nicht gefunden."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SMTP Listener
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
$listenPort = [int]$config.Smtp.Port
|
||||||
|
|
||||||
|
try {
|
||||||
|
$listeners = @(Get-NetTCPConnection -LocalPort $listenPort -State Listen -ErrorAction Stop)
|
||||||
|
|
||||||
|
if ($listeners.Count -gt 0) {
|
||||||
|
$owners = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique)
|
||||||
|
Write-Result OK "SMTP Listener aktiv auf TCP $listenPort (PID: $($owners -join ', '))."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Kein Listener auf TCP $listenPort."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Fallback falls Get-NetTCPConnection nicht verfügbar / eingeschränkt.
|
||||||
|
try {
|
||||||
|
$tcp = New-Object System.Net.Sockets.TcpClient
|
||||||
|
$result = $tcp.BeginConnect("127.0.0.1", $listenPort, $null, $null)
|
||||||
|
|
||||||
|
if ($result.AsyncWaitHandle.WaitOne(2000)) {
|
||||||
|
$tcp.EndConnect($result)
|
||||||
|
Write-Result OK "SMTP Listener auf 127.0.0.1:$listenPort erreichbar."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "SMTP Listener auf 127.0.0.1:$listenPort nicht erreichbar."
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcp.Close()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Result FAIL "SMTP Listener auf TCP $listenPort nicht erreichbar."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Graph App-only Auth
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ($graphModule -and $cert -and $cert.HasPrivateKey) {
|
||||||
|
try {
|
||||||
|
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
|
||||||
|
|
||||||
|
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
|
||||||
|
Connect-MgGraph `
|
||||||
|
-TenantId $config.Graph.TenantId `
|
||||||
|
-ClientId $config.Graph.ClientId `
|
||||||
|
-Certificate $cert `
|
||||||
|
-NoWelcome | Out-Null
|
||||||
|
|
||||||
|
$ctx = Get-MgContext
|
||||||
|
|
||||||
|
if ($ctx -and
|
||||||
|
$ctx.AuthType -eq "AppOnly" -and
|
||||||
|
$ctx.ClientId -eq $config.Graph.ClientId -and
|
||||||
|
$ctx.TenantId -eq $config.Graph.TenantId) {
|
||||||
|
|
||||||
|
Write-Result OK "Microsoft Graph App-only Anmeldung erfolgreich."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result FAIL "Graph-Verbindung vorhanden, aber Kontext entspricht nicht der Relay-App."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Result FAIL "Microsoft Graph App-only Anmeldung fehlgeschlagen: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Result WARN "Graph App-only Test übersprungen, da Modul/Zertifikat/Private Key nicht vollständig verfügbar sind."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Optionale echte SMTP-Testmail
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if ($SendTestMail) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($TestRecipient)) {
|
||||||
|
Write-Result FAIL "-SendTestMail wurde angegeben, aber -TestRecipient fehlt."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
$testFrom = [string]$config.Graph.SenderMailbox
|
||||||
|
$smtpAuthPassword = $null
|
||||||
|
|
||||||
|
if ($authRequired -and [string]::IsNullOrWhiteSpace($SmtpUsername)) {
|
||||||
|
$SmtpUsername = Read-Host "SMTP-Benutzer für Health-Check-Testmail"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SmtpUsername)) {
|
||||||
|
$secureSmtpPassword = Read-Host "SMTP-Passwort für '$SmtpUsername'" -AsSecureString
|
||||||
|
$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureSmtpPassword)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$smtpAuthPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Result INFO "Sende SMTP-Testmail über 127.0.0.1:$listenPort an $TestRecipient ..."
|
||||||
|
|
||||||
|
[void](Send-RawSmtpTestMail `
|
||||||
|
-Server "127.0.0.1" `
|
||||||
|
-Port $listenPort `
|
||||||
|
-From $testFrom `
|
||||||
|
-To $TestRecipient `
|
||||||
|
-Username $SmtpUsername `
|
||||||
|
-Password $smtpAuthPassword)
|
||||||
|
|
||||||
|
Write-Result OK "SMTP-Testmail wurde vom Relay mit 250 Queued angenommen."
|
||||||
|
Write-Result INFO "Die endgültige Graph-/M365-Zustellung bitte im Relay-Log bzw. Empfängerpostfach prüfen."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Result FAIL "SMTP-Testmail fehlgeschlagen: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$smtpAuthPassword = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Zusammenfassung
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " Ergebnis" -ForegroundColor Cyan
|
||||||
|
Write-Host "==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host (" OK: {0}" -f $script:OkCount) -ForegroundColor Green
|
||||||
|
Write-Host (" WARN: {0}" -f $script:WarnCount) -ForegroundColor Yellow
|
||||||
|
Write-Host (" FAIL: {0}" -f $script:FailCount) -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if ($script:FailCount -gt 0) {
|
||||||
|
Write-Host "Gesamtstatus: FEHLER" -ForegroundColor Red
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
elseif ($script:WarnCount -gt 0) {
|
||||||
|
Write-Host "Gesamtstatus: WARNUNG" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Gesamtstatus: OK" -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user