473 lines
18 KiB
PowerShell
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 ==="
|