mail-organizer/scripts/Process-MailRules.ps1

473 lines
18 KiB
PowerShell

#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 = @"
<h3>Mail-Organizer Alert</h3>
<p><strong>Regel:</strong> $($rule.name)</p>
<p><strong>E-Mail:</strong> $($mail.subject)</p>
<p><strong>Von:</strong> $($mail.from.emailAddress.address)</p>
<p><strong>Datum:</strong> $($mail.receivedDateTime)</p>
<h4>Befunde:</h4>
<ul>
$($reviewResult.Findings | ForEach-Object { "<li>$_</li>" } | Out-String)
</ul>
"@
if ($isDryRun) {
Write-Log "[DRY RUN] Wuerde Benachrichtigung senden an $notifyTo" -Level Warn
$mailProtocol.actions += @{ action = "alert"; status = "dryrun"; to = $notifyTo }
}
else {
try {
Send-NotificationMail -Settings $settingsHash -To $notifyTo -Subject $notifySubject -Body $htmlBody
$mailProtocol.actions += @{ action = "alert"; status = "ok"; to = $notifyTo; findings = @($reviewResult.Findings) }
}
catch {
Write-Log "Fehler beim Senden der Benachrichtigung: $_" -Level Error
$mailProtocol.actions += @{ action = "alert"; status = "error"; error = "$_" }
$totalErrors++
}
}
}
# 5. Erfolgsaktionen
if (-not $isDryRun -and $rule.actions.onSuccess) {
$success = $rule.actions.onSuccess
$successActions = @()
if ($success.markAsRead) {
Set-MessageRead -Settings $settingsHash -MessageId $mail.id
$successActions += "markAsRead"
}
if ($success.addCategory) {
Add-MessageCategory -Settings $settingsHash -MessageId $mail.id -Categories @($success.addCategory)
$successActions += "addCategory:$($success.addCategory)"
}
if ($success.moveToFolder) {
Move-Message -Settings $settingsHash -MessageId $mail.id -DestinationFolder $success.moveToFolder
$successActions += "moveToFolder:$($success.moveToFolder)"
}
$mailProtocol.actions += @{ action = "onSuccess"; status = "ok"; details = $successActions }
}
$ruleProtocol.mails += $mailProtocol
$totalProcessed++
}
$protocol.rules += $ruleProtocol
}
$protocol.summary.processed = $totalProcessed
$protocol.summary.alerts = $totalAlerts
$protocol.summary.errors = $totalErrors
# --- Protokoll schreiben ---
$protocolFile = Join-Path $protocolDir "run_$($runTimestamp.ToString('yyyy-MM-dd_HH-mm-ss')).json"
$protocol | ConvertTo-Json -Depth 10 | Set-Content -Path $protocolFile -Encoding UTF8
Write-Log "Protokoll geschrieben: $protocolFile" -Level Info
# --- HTML-Gesamtprotokoll generieren ---
$allProtocols = @()
Get-ChildItem -Path $protocolDir -Filter "run_*.json" | Sort-Object Name -Descending | ForEach-Object {
$content = Get-Content $_.FullName -Raw -Encoding UTF8
$allProtocols += $content
}
$jsonArray = "[" + ($allProtocols -join ",") + "]"
$htmlFile = Join-Path (Split-Path $protocolDir -Parent) "protocol.html"
& (Join-Path $scriptRoot "Generate-ProtocolHtml.ps1") -JsonData $jsonArray -OutputPath $htmlFile
Write-Log "HTML-Protokoll aktualisiert: $htmlFile" -Level Info
Write-Log "=== Fertig: $totalProcessed E-Mail(s) verarbeitet, $totalAlerts Alert(s), $totalErrors Fehler ==="