diff --git a/CLAUDE.md b/CLAUDE.md index abba140..cb14cc4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,10 @@ PowerShell-based automation that reads emails via Microsoft Graph API and files ``` scripts/ - Process-MailRules.ps1 # Main entry point — processes emails against rules + Process-MailRules.ps1 # Main entry point -- processes emails against rules + Run-ServiceLoop.ps1 # Service wrapper -- infinite loop calling Process-MailRules + Install-Service.ps1 # Registers Windows service via NSSM (run as Admin) + Uninstall-Service.ps1 # Removes Windows service via NSSM (run as Admin) Test-SharePointUpload.ps1 # Manual test script for SharePoint upload Test-Connection.ps1 # Connection/auth test GraphHelper.psm1 # Graph API auth + mail operations diff --git a/config/rules.json b/config/rules.json index 3da27f3..e111620 100644 --- a/config/rules.json +++ b/config/rules.json @@ -116,6 +116,62 @@ "unreadOnly": false }, + "actions": { + "onSuccess": { + "markAsRead": true, + "moveToFolder": "deleteditems" + } + } + }, + { + "id": "sfuserconnector-results-loeschen", + "enabled": true, + "name": "SFUserConnector Connector Results loeschen", + "description": "SFUserConnector Connector Results von SuccessFactors in den Papierkorb verschieben", + + "match": { + "from": "learning@successfactors.eu", + "subject": "SFUserConnector - Connector Results", + "unreadOnly": false + }, + + "actions": { + "onSuccess": { + "markAsRead": true, + "moveToFolder": "deleteditems" + } + } + }, + { + "id": "sap-user-zaduser-loeschen", + "enabled": true, + "name": "SAP-User nicht in ZADUSER loeschen", + "description": "Automatische SAP-User-nicht-in-ZADUSER-Mails von noreply@krah-gruppe.de in den Papierkorb verschieben", + + "match": { + "from": "noreply@krah-gruppe.de", + "subject": "SAP-User nicht in ZADUSER", + "unreadOnly": false + }, + + "actions": { + "onSuccess": { + "markAsRead": true, + "moveToFolder": "deleteditems" + } + } + }, + { + "id": "ebnerstolz-newsletter-loeschen", + "enabled": true, + "name": "Ebner Stolz Newsletter loeschen", + "description": "Newsletter von Ebner Stolz in den Papierkorb verschieben", + + "match": { + "from": "newsletter@newsletter.ebnerstolz.de", + "unreadOnly": false + }, + "actions": { "onSuccess": { "markAsRead": true, diff --git a/scripts/Install-Service.ps1 b/scripts/Install-Service.ps1 new file mode 100644 index 0000000..8a368fd --- /dev/null +++ b/scripts/Install-Service.ps1 @@ -0,0 +1,100 @@ +#Requires -Version 5.1 +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Installs the MailOrganizer Windows service via NSSM. + +.DESCRIPTION + Registers a Windows service that runs Run-ServiceLoop.ps1 via NSSM. + NSSM must be available in PATH or specified via -NssmPath. + +.PARAMETER ServiceName + Name of the Windows service (default: MailOrganizer) + +.PARAMETER NssmPath + Path to nssm.exe (default: looks in PATH, then tools\nssm.exe) + +.EXAMPLE + .\Install-Service.ps1 + .\Install-Service.ps1 -ServiceName "MailOrganizer-Test" -NssmPath "C:\tools\nssm.exe" +#> + +param( + [string]$ServiceName = "MailOrganizer", + [string]$NssmPath +) + +$ErrorActionPreference = "Stop" + +# --- Locate NSSM --- +if ($NssmPath -and (Test-Path $NssmPath)) { + $nssm = $NssmPath +} +elseif (Get-Command nssm -ErrorAction SilentlyContinue) { + $nssm = (Get-Command nssm).Source +} +else { + $toolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) "tools\nssm.exe" + if (Test-Path $toolsPath) { + $nssm = $toolsPath + } + else { + Write-Host "ERROR: NSSM not found. Provide -NssmPath, add to PATH, or place in tools\nssm.exe" -ForegroundColor Red + exit 1 + } +} + +Write-Host "Using NSSM: $nssm" + +# --- Check if service already exists --- +$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existing) { + Write-Host "ERROR: Service '$ServiceName' already exists (Status: $($existing.Status)). Uninstall first." -ForegroundColor Red + exit 1 +} + +# --- Paths --- +$scriptDir = $PSScriptRoot +$projectRoot = Split-Path $scriptDir -Parent +$loopScript = Join-Path $scriptDir "Run-ServiceLoop.ps1" +$logDir = Join-Path $projectRoot "logs" + +if (-not (Test-Path $logDir)) { + New-Item -ItemType Directory -Path $logDir -Force | Out-Null +} + +$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe" + +# --- Install service --- +Write-Host "Installing service '$ServiceName'..." + +& $nssm install $ServiceName $powershell "-ExecutionPolicy Bypass -NoProfile -File `"$loopScript`"" +if ($LASTEXITCODE -ne 0) { throw "NSSM install failed" } + +# Configure working directory +& $nssm set $ServiceName AppDirectory $scriptDir + +# Configure logging +$stdoutLog = Join-Path $logDir "service_stdout.log" +$stderrLog = Join-Path $logDir "service_stderr.log" +& $nssm set $ServiceName AppStdout $stdoutLog +& $nssm set $ServiceName AppStderr $stderrLog +& $nssm set $ServiceName AppStdoutCreationDisposition 4 +& $nssm set $ServiceName AppStderrCreationDisposition 4 +& $nssm set $ServiceName AppRotateFiles 1 +& $nssm set $ServiceName AppRotateOnline 1 +& $nssm set $ServiceName AppRotateBytes 5242880 + +# Set startup type to Automatic +& $nssm set $ServiceName Start SERVICE_AUTO_START + +# Set description +& $nssm set $ServiceName Description "Mail-Organizer: Processes emails and files attachments to SharePoint" + +Write-Host "" +Write-Host "Service '$ServiceName' installed successfully." -ForegroundColor Green +Write-Host " Stdout log: $stdoutLog" +Write-Host " Stderr log: $stderrLog" +Write-Host "" +Write-Host "To start: Start-Service $ServiceName" +Write-Host "To check: Get-Service $ServiceName" diff --git a/scripts/Run-ServiceLoop.ps1 b/scripts/Run-ServiceLoop.ps1 new file mode 100644 index 0000000..247a1ec --- /dev/null +++ b/scripts/Run-ServiceLoop.ps1 @@ -0,0 +1,93 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Service loop wrapper -- runs Process-MailRules.ps1 at a configurable interval. + +.DESCRIPTION + Designed to be launched by NSSM as a Windows service. Runs Process-MailRules.ps1 + in an infinite loop, sleeping ServiceIntervalMinutes between runs. + Handles graceful shutdown via Ctrl+C / console close events. + +.PARAMETER ConfigPath + Path to config directory (default: ..\config relative to script) + +.EXAMPLE + .\Run-ServiceLoop.ps1 + .\Run-ServiceLoop.ps1 -ConfigPath "C:\work\mail-organizer\config" +#> + +param( + [string]$ConfigPath = (Join-Path $PSScriptRoot "..\config") +) + +# --- Load Logger --- +Import-Module (Join-Path $PSScriptRoot "Logger.psm1") -Force + +$logDir = Join-Path (Split-Path $ConfigPath -Parent) "logs" +Initialize-Logger -LogDirectory $logDir -Level "Info" + +# --- Read interval from settings --- +$settingsFile = Join-Path $ConfigPath "settings.json" +if (-not (Test-Path $settingsFile)) { + Write-Log "Settings not found: $settingsFile" -Level Error + exit 1 +} + +$settings = Get-Content $settingsFile -Raw | ConvertFrom-Json +$intervalMinutes = if ($settings.ServiceIntervalMinutes) { $settings.ServiceIntervalMinutes } else { 5 } + +# --- Graceful shutdown flag --- +$script:Running = $true + +$cancelHandler = { + $script:Running = $false + Write-Log "Shutdown signal received, stopping after current run..." -Level Warn +} + +# Register Ctrl+C / console close handler +try { + [Console]::TreatControlCAsInput = $false + $null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action $cancelHandler -ErrorAction SilentlyContinue +} catch { + # Ignore if not supported in this host +} +trap { + $script:Running = $false + Write-Log "Service loop terminated by signal" -Level Warn + break +} + +# --- Main loop --- +$processScript = Join-Path $PSScriptRoot "Process-MailRules.ps1" + +if (-not (Test-Path $processScript)) { + Write-Log "Process-MailRules.ps1 not found: $processScript" -Level Error + exit 1 +} + +Write-Log "Service loop started (interval: $intervalMinutes min)" + +while ($script:Running) { + Write-Log "Running Process-MailRules.ps1..." + + try { + & $processScript -ConfigPath $ConfigPath + Write-Log "Process-MailRules.ps1 completed successfully" + } + catch { + Write-Log "Process-MailRules.ps1 failed: $_" -Level Error + } + + if (-not $script:Running) { break } + + Write-Log "Waiting $intervalMinutes minute(s) until next run..." + + # Sleep in 1-second intervals for responsive shutdown + $sleepSeconds = $intervalMinutes * 60 + for ($i = 0; $i -lt $sleepSeconds; $i++) { + if (-not $script:Running) { break } + Start-Sleep -Seconds 1 + } +} + +Write-Log "Service loop stopped" diff --git a/scripts/Uninstall-Service.ps1 b/scripts/Uninstall-Service.ps1 new file mode 100644 index 0000000..ae40abe --- /dev/null +++ b/scripts/Uninstall-Service.ps1 @@ -0,0 +1,62 @@ +#Requires -Version 5.1 +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Stops and removes the MailOrganizer Windows service via NSSM. + +.PARAMETER ServiceName + Name of the Windows service (default: MailOrganizer) + +.PARAMETER NssmPath + Path to nssm.exe (default: looks in PATH, then tools\nssm.exe) + +.EXAMPLE + .\Uninstall-Service.ps1 + .\Uninstall-Service.ps1 -ServiceName "MailOrganizer-Test" +#> + +param( + [string]$ServiceName = "MailOrganizer", + [string]$NssmPath +) + +$ErrorActionPreference = "Stop" + +# --- Locate NSSM --- +if ($NssmPath -and (Test-Path $NssmPath)) { + $nssm = $NssmPath +} +elseif (Get-Command nssm -ErrorAction SilentlyContinue) { + $nssm = (Get-Command nssm).Source +} +else { + $toolsPath = Join-Path (Split-Path $PSScriptRoot -Parent) "tools\nssm.exe" + if (Test-Path $toolsPath) { + $nssm = $toolsPath + } + else { + Write-Host "ERROR: NSSM not found. Provide -NssmPath, add to PATH, or place in tools\nssm.exe" -ForegroundColor Red + exit 1 + } +} + +# --- Check if service exists --- +$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if (-not $existing) { + Write-Host "Service '$ServiceName' not found. Nothing to uninstall." -ForegroundColor Yellow + exit 0 +} + +# --- Stop service if running --- +if ($existing.Status -eq "Running") { + Write-Host "Stopping service '$ServiceName'..." + & $nssm stop $ServiceName + Start-Sleep -Seconds 2 +} + +# --- Remove service --- +Write-Host "Removing service '$ServiceName'..." +& $nssm remove $ServiceName confirm +if ($LASTEXITCODE -ne 0) { throw "NSSM remove failed" } + +Write-Host "Service '$ServiceName' removed successfully." -ForegroundColor Green