#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS First-Run Setup für SMTPGraphRelay. .DESCRIPTION - Installiert notwendige Microsoft Graph PowerShell Module - Meldet interaktiv einen Entra-Administrator an - Erstellt ein selbstsigniertes Zertifikat in LocalMachine\My - Erstellt eine Entra ID App Registration - Fügt das Zertifikat als Credential hinzu - Vergibt Microsoft Graph Application Permission Mail.Send - Erteilt Admin Consent über AppRoleAssignment - Schreibt config.json - Legt Firewallregel und Scheduled Task an #> [CmdletBinding()] param() $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # ------------------------------------------------------------------------------ # WICHTIG: Dieses Setup muss mit Windows PowerShell 5.1 gestartet werden. # Der eigentliche SMTPGraphRelay Scheduled Task verwendet ebenfalls powershell.exe # (Windows PowerShell 5.1). Wird das Setup stattdessen mit PowerShell 7 / pwsh.exe # ausgeführt, können Microsoft.Graph-Module im falschen Modulpfad landen und sind # später für den SYSTEM-Task unter Windows PowerShell 5.1 nicht sichtbar. # ------------------------------------------------------------------------------ if ($PSVersionTable.PSEdition -ne "Desktop" -or $PSVersionTable.PSVersion.Major -ne 5) { Write-Host "" Write-Host "==========================================================" -ForegroundColor Red Write-Host " FALSCHE POWERSHELL-VERSION" -ForegroundColor Red Write-Host "==========================================================" -ForegroundColor Red Write-Host "" Write-Host "Dieses Setup muss mit Windows PowerShell 5.1 ausgefuehrt werden." -ForegroundColor Yellow Write-Host "" Write-Host "Aktuell erkannt:" -ForegroundColor Cyan Write-Host " Edition: $($PSVersionTable.PSEdition)" Write-Host " Version: $($PSVersionTable.PSVersion)" Write-Host "" Write-Host "Bitte eine klassische Windows PowerShell oeffnen:" -ForegroundColor Cyan Write-Host " C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" Write-Host "" Write-Host "Danach das Setup dort erneut starten." -ForegroundColor Yellow Write-Host "" Read-Host "Enter druecken zum Beenden" exit 1 } Write-Host "Windows PowerShell erkannt: $($PSVersionTable.PSVersion)" -ForegroundColor Green Write-Host "" function Read-Default { param([string]$Prompt, [string]$Default) $value = Read-Host "$Prompt [Standard: $Default]" if ([string]::IsNullOrWhiteSpace($value)) { return $Default } return $value } function Convert-CertToKeyCredential { param([Parameter(Mandatory)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) return @{ Type = "AsymmetricX509Cert" Usage = "Verify" Key = $Certificate.GetRawCertData() DisplayName = "SMTPGraphRelay Certificate" StartDateTime = $Certificate.NotBefore.ToUniversalTime() EndDateTime = $Certificate.NotAfter.ToUniversalTime() } } Write-Host "==========================================================" -ForegroundColor Cyan Write-Host " SMTPGraphRelay - First Run / Entra ID Setup" -ForegroundColor Cyan Write-Host "==========================================================" -ForegroundColor Cyan Write-Host "" Write-Host "Schritt 0: Voraussetzungen prüfen..." -ForegroundColor Cyan $modules = @( "Microsoft.Graph.Authentication", "Microsoft.Graph.Applications" ) foreach ($module in $modules) { if (-not (Get-Module -ListAvailable -Name $module)) { Write-Host "-> $module fehlt. Wird für alle Benutzer installiert..." -ForegroundColor Yellow Install-Module $module -Scope AllUsers -Repository PSGallery -Force -AllowClobber } } Import-Module Microsoft.Graph.Authentication -ErrorAction Stop Import-Module Microsoft.Graph.Applications -ErrorAction Stop Write-Host "-> Voraussetzungen erfüllt." -ForegroundColor Green Write-Host "" Write-Host "Schritt 1: Relay-Konfiguration erfassen" -ForegroundColor Cyan $AppName = Read-Default "-> Name der Entra App" "SMTPGraphRelay" $SenderMailbox = Read-Host "-> M365-Absenderpostfach (z.B. smtp-relay@firma.de)" while ([string]::IsNullOrWhiteSpace($SenderMailbox) -or $SenderMailbox -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') { $SenderMailbox = Read-Host "-> Bitte eine gültige Mailadresse eingeben" } $ListenAddress = Read-Default "-> Lokale Listen-IP" "0.0.0.0" $Port = [int](Read-Default "-> SMTP-Port" "2525") $AllowedNetworksText = Read-Default "-> Erlaubte Netze, mit Komma getrennt" "127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" $AllowedNetworks = @($AllowedNetworksText -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) Write-Host "" Write-Host "Schritt 2: Erzeuge Zertifikat für unbeaufsichtigte Graph-Anmeldung..." -ForegroundColor Cyan $certSubject = "CN=SMTPGraphRelay-$env:COMPUTERNAME" $cert = New-SelfSignedCertificate ` -Subject $certSubject ` -CertStoreLocation "Cert:\LocalMachine\My" ` -KeyAlgorithm RSA ` -KeyLength 2048 ` -HashAlgorithm SHA256 ` -KeyExportPolicy NonExportable ` -KeySpec Signature ` -NotAfter (Get-Date).AddYears(2) Write-Host "-> Zertifikat erstellt: $($cert.Thumbprint)" -ForegroundColor Green Write-Host "" Write-Host "Schritt 3: Mit Microsoft Graph anmelden..." -ForegroundColor Cyan Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null # Application.ReadWrite.All: App + Service Principal erstellen # AppRoleAssignment.ReadWrite.All: Mail.Send als AppRoleAssignment (Admin Consent) erteilen Connect-MgGraph -Scopes "Application.ReadWrite.All","AppRoleAssignment.ReadWrite.All" -NoWelcome $TenantId = (Get-MgContext).TenantId Write-Host "-> Verbunden mit Tenant: $TenantId" -ForegroundColor Green Write-Host "" Write-Host "Schritt 4: Microsoft Graph Mail.Send Application Permission ermitteln..." -ForegroundColor Cyan $GraphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" if (-not $GraphSp) { throw "Microsoft Graph Service Principal wurde im Tenant nicht gefunden." } $MailSend = $GraphSp.AppRoles | Where-Object { $_.Value -eq "Mail.Send" -and $_.AllowedMemberTypes -contains "Application" } | Select-Object -First 1 if (-not $MailSend) { throw "Graph Application Permission Mail.Send wurde nicht gefunden." } Write-Host "-> Mail.Send AppRole-ID: $($MailSend.Id)" -ForegroundColor Green Write-Host "" Write-Host "Schritt 5: Entra ID App Registration erstellen..." -ForegroundColor Cyan $AppParams = @{ DisplayName = $AppName SignInAudience = "AzureADMyOrg" RequiredResourceAccess = @( @{ ResourceAppId = "00000003-0000-0000-c000-000000000000" ResourceAccess = @( @{ Id = $MailSend.Id Type = "Role" } ) } ) KeyCredentials = @( (Convert-CertToKeyCredential -Certificate $cert) ) } $App = New-MgApplication -BodyParameter $AppParams Write-Host "-> App erstellt: $($App.AppId)" -ForegroundColor Green $Sp = $null for ($i = 0; $i -lt 10 -and -not $Sp; $i++) { try { $Sp = New-MgServicePrincipal -AppId $App.AppId } catch { Start-Sleep -Seconds 2 } } if (-not $Sp) { throw "Service Principal konnte nicht erstellt werden." } Write-Host "-> Service Principal erstellt: $($Sp.Id)" -ForegroundColor Green Write-Host "" Write-Host "Schritt 6: Admin Consent für Mail.Send erteilen..." -ForegroundColor Cyan New-MgServicePrincipalAppRoleAssignment ` -ServicePrincipalId $Sp.Id ` -PrincipalId $Sp.Id ` -ResourceId $GraphSp.Id ` -AppRoleId $MailSend.Id | Out-Null Write-Host "-> Mail.Send wurde als Application Permission erteilt." -ForegroundColor Green Write-Host "" Write-Host "Schritt 7: config.json schreiben..." -ForegroundColor Cyan $config = [ordered]@{ Smtp = [ordered]@{ ListenAddress = $ListenAddress Port = $Port Hostname = $env:COMPUTERNAME AllowedNetworks = $AllowedNetworks MaxMessageSizeMB = 25 ClientTimeoutSeconds = 120 } Graph = [ordered]@{ TenantId = $TenantId ClientId = $App.AppId CertificateThumbprint = $cert.Thumbprint SenderMailbox = $SenderMailbox ForceSender = $true } Queue = [ordered]@{ PollSeconds = 10 MaxRetries = 8 RetryMinutes = @(1,5,15,30,60,120,240,480) } Paths = [ordered]@{ Queue = "queue" Failed = "failed" Logs = "logs" } } $configPath = Join-Path $PSScriptRoot "config.json" $config | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $configPath -Encoding UTF8 foreach ($dir in @("queue","failed","logs")) { New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot $dir) -Force | Out-Null } Write-Host "-> Konfiguration: $configPath" -ForegroundColor Green Write-Host "" Write-Host "Schritt 8: Windows Firewall konfigurieren..." -ForegroundColor Cyan $ruleName = "SMTPGraphRelay TCP $Port" Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue New-NetFirewallRule ` -DisplayName $ruleName ` -Direction Inbound ` -Action Allow ` -Protocol TCP ` -LocalPort $Port ` -Profile Any | Out-Null Write-Host "-> Firewallregel erstellt." -ForegroundColor Green Write-Host "" Write-Host "Schritt 9: Autostart als Scheduled Task anlegen..." -ForegroundColor Cyan $taskName = "SMTPGraphRelay" $scriptPath = Join-Path $PSScriptRoot "SMTPGraphRelay.ps1" $psExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" $action = New-ScheduledTaskAction ` -Execute $psExe ` -Argument "-NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" $trigger = New-ScheduledTaskTrigger -AtStartup $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -StartWhenAvailable ` -RestartCount 5 ` -RestartInterval (New-TimeSpan -Minutes 1) ` -ExecutionTimeLimit ([TimeSpan]::Zero) Register-ScheduledTask ` -TaskName $taskName ` -Action $action ` -Trigger $trigger ` -Principal $principal ` -Settings $settings ` -Description "Lokaler SMTP Store-and-Forward Relay zu Microsoft 365 via Microsoft Graph" ` -Force | Out-Null Write-Host "-> Scheduled Task '$taskName' erstellt." -ForegroundColor Green Write-Host "" Write-Host "Schritt 10: Teste App-only Graph-Anmeldung..." -ForegroundColor Cyan Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Connect-MgGraph ` -TenantId $TenantId ` -ClientId $App.AppId ` -Certificate $cert ` -NoWelcome | Out-Null $ctx = Get-MgContext if ($ctx.AuthType -ne "AppOnly") { throw "App-only Graph-Anmeldung konnte nicht bestätigt werden." } Write-Host "-> App-only Graph-Anmeldung funktioniert." -ForegroundColor Green Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null Write-Host "" Write-Host "==========================================================" -ForegroundColor Green Write-Host " SMTPGraphRelay wurde eingerichtet." -ForegroundColor Green Write-Host "==========================================================" -ForegroundColor Green Write-Host "" Write-Host " Tenant ID: $TenantId" -ForegroundColor Yellow Write-Host " Client ID: $($App.AppId)" -ForegroundColor Yellow Write-Host " Zertifikat: $($cert.Thumbprint)" -ForegroundColor Yellow Write-Host " Graph-Absender: $SenderMailbox" -ForegroundColor Yellow Write-Host " SMTP Listener: $ListenAddress`:$Port" -ForegroundColor Yellow Write-Host "" Write-Host "WICHTIG:" -ForegroundColor Yellow Write-Host "Mail.Send als Application Permission ist tenantweit mächtig." Write-Host "Diese V1 erzwingt lokal SenderMailbox='$SenderMailbox', begrenzt die" Write-Host "Graph-Berechtigung selbst aber noch nicht per Exchange Application RBAC." Write-Host "" Write-Host "Task starten mit:" -ForegroundColor Cyan Write-Host " Start-ScheduledTask -TaskName `"$taskName`"" Write-Host "" Write-Host "Logs:" -ForegroundColor Cyan Write-Host " $(Join-Path $PSScriptRoot 'logs\SMTPGraphRelay.log')" Write-Host "" $startNow = Read-Host "Relay jetzt starten? [J/n]" if ([string]::IsNullOrWhiteSpace($startNow) -or $startNow -match '^(?i)j|ja|y|yes$') { Start-ScheduledTask -TaskName $taskName Start-Sleep -Seconds 2 Write-Host "-> Task gestartet." -ForegroundColor Green } Read-Host "Enter drücken zum Beenden"