#Requires -Version 5.1 <# .SYNOPSIS Mail-Organizer Hauptscript -- verarbeitet E-Mails anhand definierter Regeln. .DESCRIPTION Liest E-Mails aus der konfigurierten Mailbox, prueft sie gegen die Regeln in config/rules.json und fuehrt die definierten Aktionen aus: - Anhaenge speichern - Inhalte pruefen (Keywords, Schwellenwerte) - Bei Auffaelligkeiten benachrichtigen - E-Mails als gelesen markieren / kategorisieren .PARAMETER ConfigPath Pfad zum config-Verzeichnis (Standard: ..\config relativ zum Script) .PARAMETER DryRun Wenn gesetzt, werden keine Aenderungen durchgefuehrt (nur Simulation) .EXAMPLE .\Process-MailRules.ps1 .\Process-MailRules.ps1 -DryRun .\Process-MailRules.ps1 -ConfigPath "C:\work\KRAH\mail-organizer\config" #> param( [string]$ConfigPath = (Join-Path $PSScriptRoot "..\config"), [switch]$DryRun ) # --- Module laden --- $scriptRoot = $PSScriptRoot Import-Module (Join-Path $scriptRoot "Logger.psm1") -Force Import-Module (Join-Path $scriptRoot "GraphHelper.psm1") -Force Import-Module (Join-Path $scriptRoot "SharePointHelper.psm1") -Force # --- Konfiguration laden --- $settingsFile = Join-Path $ConfigPath "settings.json" $rulesFile = Join-Path $ConfigPath "rules.json" if (-not (Test-Path $settingsFile)) { throw "Settings nicht gefunden: $settingsFile -- siehe docs/Azure-App-Registration-Anleitung.md" } if (-not (Test-Path $rulesFile)) { throw "Regeln nicht gefunden: $rulesFile" } $settings = Get-Content $settingsFile -Raw | ConvertFrom-Json $settingsHash = @{ TenantId = $settings.TenantId ClientId = $settings.ClientId ClientSecret = $settings.ClientSecret MailboxUser = $settings.MailboxUser } $rulesConfig = Get-Content $rulesFile -Raw | ConvertFrom-Json $rules = $rulesConfig.rules | Where-Object { $_.enabled -eq $true } $targetsFile = Join-Path $ConfigPath "sharepoint-targets.json" $spTargets = @{} if (Test-Path $targetsFile) { $targetsConfig = Get-Content $targetsFile -Raw | ConvertFrom-Json foreach ($t in $targetsConfig.targets) { $spTargets[$t.id] = $t } Write-Log "SharePoint-Targets geladen: $($spTargets.Count) Ziel(e)" -Level Debug } if ($DryRun -or $settings.DryRun) { $isDryRun = $true Write-Host "*** DRY RUN MODUS -- keine Aenderungen werden durchgefuehrt ***" -ForegroundColor Cyan } else { $isDryRun = $false } # --- Logger starten --- $logDir = Join-Path (Split-Path $ConfigPath -Parent) "logs" Initialize-Logger -LogDirectory $logDir -Level $settings.LogLevel -RetentionDays $settings.LogRetentionDays Write-Log "Konfiguration geladen: $($rules.Count) aktive Regel(n)" # --- Hilfsfunktionen --- function Test-MailMatchesRule { <# .SYNOPSIS Prueft ob eine E-Mail zu einer Regel passt. #> param($Mail, $Rule) $match = $Rule.match # Kategorie pruefen if ($match.category) { $categories = @($Mail.categories) if ($match.category -notin $categories) { return $false } } # Absender pruefen (Wildcard) if ($match.from) { $sender = $Mail.from.emailAddress.address if ($sender -notlike $match.from) { return $false } } # Betreff pruefen (Wildcard) if ($match.subject) { if ($Mail.subject -notlike $match.subject) { return $false } } # Anhang pruefen if ($match.hasAttachment -eq $true) { if (-not $Mail.hasAttachments) { return $false } } return $true } function Expand-PathTemplate { <# .SYNOPSIS Ersetzt Platzhalter in Pfaden: {year}, {month}, {date}, {sender} #> param( [string]$Template, $Mail ) $received = [DateTime]::Parse($Mail.receivedDateTime) $sender = ($Mail.from.emailAddress.address -split "@")[0] return $Template ` -replace '\{year\}', $received.ToString("yyyy") ` -replace '\{month\}', $received.ToString("MM") ` -replace '\{date\}', $received.ToString("yyyy-MM-dd") ` -replace '\{sender\}', $sender } function Invoke-ContentReview { <# .SYNOPSIS Prueft den Inhalt einer E-Mail oder ihrer Anhaenge auf Auffaelligkeiten. .OUTPUTS Hashtable mit: IsAlert (bool), Findings (string[]), Summary (string) #> param( $Mail, $ReviewConfig, [string[]]$AttachmentPaths = @() ) $findings = @() switch ($ReviewConfig.type) { "keyword" { $keywords = $ReviewConfig.alertKeywords $bodyText = $Mail.body.content # E-Mail-Body durchsuchen foreach ($kw in $keywords) { if ($bodyText -match [regex]::Escape($kw)) { $findings += "Keyword '$kw' im E-Mail-Body gefunden" } } # Anhaenge durchsuchen (Textdateien, CSV) foreach ($file in $AttachmentPaths) { $ext = [IO.Path]::GetExtension($file).ToLower() if ($ext -in @(".txt", ".csv", ".log", ".xml", ".html", ".htm")) { $content = Get-Content $file -Raw -ErrorAction SilentlyContinue if ($content) { foreach ($kw in $keywords) { if ($content -match [regex]::Escape($kw)) { $findings += "Keyword '$kw' in Anhang '$([IO.Path]::GetFileName($file))' gefunden" } } } } # PDF-Textextraktion koennte hier ergaenzt werden } } "threshold" { # Platzhalter fuer zukuenftige Schwellenwert-Pruefungen Write-Log "Threshold-Review noch nicht implementiert fuer diese Regel" -Level Warn } "custom" { # Platzhalter fuer benutzerdefinierte Review-Scripte if ($ReviewConfig.scriptPath -and (Test-Path $ReviewConfig.scriptPath)) { $result = & $ReviewConfig.scriptPath -Mail $Mail -Attachments $AttachmentPaths if ($result.Findings) { $findings += $result.Findings } } } } $threshold = if ($ReviewConfig.alertThreshold) { $ReviewConfig.alertThreshold } else { 1 } $isAlert = $findings.Count -ge $threshold return @{ IsAlert = $isAlert Findings = $findings Summary = if ($findings.Count -gt 0) { $findings -join "`n" } else { "Keine Auffaelligkeiten" } } } # --- Protokoll vorbereiten --- $protocolDir = Join-Path (Split-Path $ConfigPath -Parent) "logs\protocols" if (-not (Test-Path $protocolDir)) { New-Item -ItemType Directory -Path $protocolDir -Force | Out-Null } $runTimestamp = Get-Date $protocol = @{ runId = [guid]::NewGuid().ToString("N").Substring(0, 8) timestamp = $runTimestamp.ToString("yyyy-MM-ddTHH:mm:ss") dryRun = $isDryRun mailbox = $settings.MailboxUser rules = @() summary = @{ processed = 0; alerts = 0; errors = 0 } } # --- Hauptlogik --- $totalProcessed = 0 $totalAlerts = 0 $totalErrors = 0 foreach ($rule in $rules) { Write-Log "--- Regel: $($rule.name) ---" $ruleProtocol = @{ ruleId = $rule.id ruleName = $rule.name mails = @() error = $null } # Filter fuer Graph API bauen $graphFilter = $null if ($rule.match.category) { $graphFilter = "categories/any(c:c eq '$($rule.match.category)')" } try { $unreadOnly = if ($rule.match.PSObject.Properties['unreadOnly'] -and $rule.match.unreadOnly -eq $false) { $false } else { $true } $top = if ($rule.match.top) { $rule.match.top } else { 250 } $params = @{ Settings = $settingsHash; Filter = $graphFilter; Top = $top } if ($unreadOnly) { $params.UnreadOnly = $true } $messages = Get-MailMessages @params } catch { Write-Log "Fehler beim Abrufen der E-Mails fuer Regel '$($rule.name)': $_" -Level Error $ruleProtocol.error = "Fehler beim Abrufen: $_" $totalErrors++ $protocol.rules += $ruleProtocol continue } $matchingMails = @($messages.value | Where-Object { Test-MailMatchesRule -Mail $_ -Rule $rule }) Write-Log "Gefunden: $($matchingMails.Count) passende E-Mail(s)" foreach ($mail in $matchingMails) { Write-Log "Verarbeite: $($mail.subject) von $($mail.from.emailAddress.address)" $mailProtocol = @{ subject = $mail.subject from = $mail.from.emailAddress.address received = $mail.receivedDateTime actions = @() status = "ok" } $savedFiles = @() # 1. Anhaenge speichern if ($rule.actions.saveAttachments) { $targetPath = Expand-PathTemplate -Template $rule.actions.saveAttachments.targetPath -Mail $mail if ($isDryRun) { Write-Log "[DRY RUN] Wuerde Anhaenge speichern nach: $targetPath" -Level Info $mailProtocol.actions += @{ action = "saveAttachments"; status = "dryrun"; target = $targetPath } } else { try { $savedFiles = Get-MailAttachments -Settings $settingsHash -MessageId $mail.id -TargetPath $targetPath Write-Log "$($savedFiles.Count) Anhang/Anhaenge gespeichert" $mailProtocol.actions += @{ action = "saveAttachments"; status = "ok"; files = $savedFiles.Count; target = $targetPath } } catch { Write-Log "Fehler beim Speichern der Anhaenge: $_" -Level Error $mailProtocol.actions += @{ action = "saveAttachments"; status = "error"; error = "$_" } $mailProtocol.status = "error" $totalErrors++ $ruleProtocol.mails += $mailProtocol continue } } } # 2. SharePoint-Upload if ($rule.actions.uploadToSharePoint) { $spAction = $rule.actions.uploadToSharePoint $targetId = $spAction.targetId if (-not $spTargets.ContainsKey($targetId)) { Write-Log "SharePoint-Target '$targetId' nicht gefunden in sharepoint-targets.json" -Level Error $mailProtocol.actions += @{ action = "uploadToSharePoint"; status = "error"; error = "Target '$targetId' nicht gefunden" } $mailProtocol.status = "error" $totalErrors++ } else { $targetConfig = $spTargets[$targetId] if ($isDryRun) { Write-Log "[DRY RUN] Wuerde Anhaenge nach SharePoint '$targetId' hochladen" -Level Info $mailProtocol.actions += @{ action = "uploadToSharePoint"; status = "dryrun"; target = $targetId } } else { $tempDir = Join-Path $env:TEMP "mail-organizer-$($mail.id.Substring(0,8))" try { $tempFiles = Get-MailAttachments -Settings $settingsHash -MessageId $mail.id -TargetPath $tempDir # Filter by file pattern $fileFilter = $spAction.fileFilter if ($fileFilter) { $tempFiles = @($tempFiles | Where-Object { (Split-Path $_ -Leaf) -like $fileFilter }) } # Build metadata hashtable from rule config $metadata = @{} if ($spAction.metadata) { foreach ($prop in $spAction.metadata.PSObject.Properties) { $metadata[$prop.Name] = $prop.Value } } # Build target config hashtable for Invoke-SharePointUpload $spTargetParam = @{ siteUrl = $targetConfig.sharepoint.siteUrl libraryName = $targetConfig.sharepoint.libraryName targetFolder = $targetConfig.sharepoint.targetFolder defaultFields = $targetConfig.defaults } $uploadedFiles = @() foreach ($file in $tempFiles) { $uploadResult = Invoke-SharePointUpload ` -Settings $settingsHash ` -TargetConfig $spTargetParam ` -FilePath $file ` -Metadata $metadata Write-Log "SharePoint-Upload OK: $($uploadResult.FileName) -> $($uploadResult.WebUrl)" $uploadedFiles += @{ name = $uploadResult.FileName; url = $uploadResult.WebUrl } } if ($tempFiles.Count -eq 0) { Write-Log "Keine Anhaenge passend zu '$fileFilter' gefunden" -Level Warn $mailProtocol.actions += @{ action = "uploadToSharePoint"; status = "warn"; message = "Keine Dateien passend zu '$fileFilter'" } } else { $mailProtocol.actions += @{ action = "uploadToSharePoint"; status = "ok"; target = $targetId; files = $uploadedFiles } } } catch { Write-Log "Fehler beim SharePoint-Upload: $_" -Level Error $mailProtocol.actions += @{ action = "uploadToSharePoint"; status = "error"; error = "$_" } $mailProtocol.status = "error" $totalErrors++ $ruleProtocol.mails += $mailProtocol continue } finally { if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } } } } # 3. Inhalt pruefen $reviewResult = @{ IsAlert = $false; Summary = "Kein Review konfiguriert" } if ($rule.actions.review -and $rule.actions.review.enabled) { $reviewResult = Invoke-ContentReview -Mail $mail -ReviewConfig $rule.actions.review -AttachmentPaths $savedFiles Write-Log "Review-Ergebnis: $($reviewResult.Summary)" } # 4. Bei Alert: Benachrichtigung if ($reviewResult.IsAlert -and $rule.actions.onAlert.sendNotification) { $totalAlerts++ $notifyTo = $settings.NotificationEmail $notifySubject = if ($rule.actions.onAlert.notificationSubject) { $rule.actions.onAlert.notificationSubject } else { "Mail-Organizer: Auffaelligkeit in '$($rule.name)'" } $htmlBody = @"
Regel: $($rule.name)
E-Mail: $($mail.subject)
Von: $($mail.from.emailAddress.address)
Datum: $($mail.receivedDateTime)