Exchange Web Services (EWS) wird für Exchange Online am 1. Oktober 2026 abgeschaltet. Wer per Skript auf Postfächer zugreift – etwa um automatisiert zugestellte Dateien abzuholen –, muss auf die Microsoft Graph API wechseln. Diese Anleitung baut die komplette Graph-Anbindung in PowerShell auf: App-Registrierung, zertifikatsbasierte Anmeldung, Lesen und Herunterladen von Anhängen sowie Mailversand – jeweils mit dem konkreten Code.
Als durchgehendes Beispiel dient ein unbeaufsichtigtes Skript, das ZIP-Anhänge aus einem Postfach herunterlädt, entpackt und anschliessend einen Report verschickt. Platzhalter wie example.com und die Tenant-/App-IDs durch eigene Werte ersetzen.
1. Voraussetzungen
Es genügen drei Module des Microsoft-Graph-SDK – nicht das gesamte Meta-Modul Microsoft.Graph:
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions -Scope
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions -Scope
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions -Scope
2. App-Registrierung in Entra ID
Unbeaufsichtigte Skripte melden nicht einen Benutzer an, sondern eine App mit eigenen Rechten (App-Only). Im Entra Admin Center unter App registrations eine neue Registrierung anlegen und ihr unter API permissions → Microsoft Graph → Application permissions zwei Rechte geben:
Mail.ReadWrite – Mails lesen und nach der Verarbeitung verschieben
Mail.Send – die Report-Mail versenden
Danach Grant admin consent klicken und Tenant-ID sowie Application (client) ID notieren.
3. Zertifikat statt Client Secret
App-Only authentifiziert per Client Secret oder Zertifikat. Für Scheduled Tasks ist das Zertifikat die sauberere Wahl: Der private Schlüssel bleibt im Zertifikatspeicher, es liegt kein Passwort im Skript. Zertifikat auf dem ausführenden Server erzeugen und den öffentlichen Teil exportieren:
$cert = New-SelfSignedCertificate -Subject "CN=eCall-Graph"
"Cert:\LocalMachine\My"
-KeyExportPolicy NonExportable -KeySpec Signature
$cert
$cert$cert = New-SelfSignedCertificate -Subject "CN=eCall-Graph"
"Cert:\LocalMachine\My"
-KeyExportPolicy NonExportable -KeySpec Signature
$cert
$cert$cert = New-SelfSignedCertificate -Subject "CN=eCall-Graph"
"Cert:\LocalMachine\My"
-KeyExportPolicy NonExportable -KeySpec Signature
$cert
$certDie exportierte .cer-Datei in der App-Registrierung unter Certificates & secrets hochladen. Das Konto des Scheduled Tasks braucht Leserecht auf den privaten Schlüssel (certlm.msc → Zertifikat → All Tasks → Manage Private Keys).
4. Zugriff auf einzelne Postfächer einschränken
Application Permissions gelten sonst tenant-weit – die App dürfte jedes Postfach im Tenant lesen. Eine Application Access Policy begrenzt sie auf eine Mail-aktivierte Sicherheitsgruppe mit den erlaubten Postfächern (Exchange Online PowerShell, einmalig):
New-ApplicationAccessPolicy -AppId "<App-ID>"
"graph-mailboxes@example.com"
-AccessRight RestrictAccess
"eCall Graph: nur Log-Postfach"
"<App-ID>""ecall-logs@example.com"New-ApplicationAccessPolicy -AppId "<App-ID>"
"graph-mailboxes@example.com"
-AccessRight RestrictAccess
"eCall Graph: nur Log-Postfach"
"<App-ID>""ecall-logs@example.com"New-ApplicationAccessPolicy -AppId "<App-ID>"
"graph-mailboxes@example.com"
-AccessRight RestrictAccess
"eCall Graph: nur Log-Postfach"
"<App-ID>""ecall-logs@example.com"5. Verbindung aufbauen
Die Anmeldung nutzt Tenant-ID, App-ID und den Zertifikat-Thumbprint – ganz ohne Benutzerinteraktion:
$TenantId = "00000000-0000-0000-0000-000000000000"
$ClientId = "00000000-0000-0000-0000-000000000000"
$Thumbprint = "0000000000000000000000000000000000000000"
$Mailbox = "ecall-logs@example.com"
Import-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId
$Thumbprint
$TenantId = "00000000-0000-0000-0000-000000000000"
$ClientId = "00000000-0000-0000-0000-000000000000"
$Thumbprint = "0000000000000000000000000000000000000000"
$Mailbox = "ecall-logs@example.com"
Import-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId
$Thumbprint
$TenantId = "00000000-0000-0000-0000-000000000000"
$ClientId = "00000000-0000-0000-0000-000000000000"
$Thumbprint = "0000000000000000000000000000000000000000"
$Mailbox = "ecall-logs@example.com"
Import-Module Microsoft.Graph.Authentication, Microsoft.Graph.Mail, Microsoft.Graph.Users.Actions
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId
$Thumbprint
6. Mails lesen und ZIP-Anhänge herunterladen
Der Kern: Posteingang durchgehen, ZIP-Anhänge speichern, entpacken und die verarbeitete Mail nach „Gelöschte Elemente“ verschieben. Entscheidend ist der Download über den /$value-Endpunkt mit Invoke-MgGraphRequest -OutputFilePath – das streamt den Rohinhalt direkt in eine Datei und funktioniert auch bei grossen Anhängen zuverlässig:
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Zielordner = "D:\Import\{0:yyyyMMdd_HHmmss}" -f (Get-Date)
$messages = Get-MgUserMessage -UserId $Mailbox -Top 100
$msg$messages
$ordner$Zielordner$msg
$ordner
$anhaenge$Mailbox$msg
$_'@odata.type''#microsoft.graph.fileAttachment'
$_'*.zip'
$att$anhaenge
$zip$ordner$att
$uri"https://graph.microsoft.com/v1.0/users/$Mailbox/messages/$msg/attachments/$att/`$value"
$uri$zip
$zip$ordner
"Geloeschte Elemente"
$Mailbox$msg"deleteditems"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Zielordner = "D:\Import\{0:yyyyMMdd_HHmmss}" -f (Get-Date)
$messages = Get-MgUserMessage -UserId $Mailbox -Top 100
$msg$messages
$ordner$Zielordner$msg
$ordner
$anhaenge$Mailbox$msg
$_'@odata.type''#microsoft.graph.fileAttachment'
$_'*.zip'
$att$anhaenge
$zip$ordner$att
$uri"https://graph.microsoft.com/v1.0/users/$Mailbox/messages/$msg/attachments/$att/`$value"
$uri$zip
$zip$ordner
"Geloeschte Elemente"
$Mailbox$msg"deleteditems"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Zielordner = "D:\Import\{0:yyyyMMdd_HHmmss}" -f (Get-Date)
$messages = Get-MgUserMessage -UserId $Mailbox -Top 100
$msg$messages
$ordner$Zielordner$msg
$ordner
$anhaenge$Mailbox$msg
$_'@odata.type''#microsoft.graph.fileAttachment'
$_'*.zip'
$att$anhaenge
$zip$ordner$att
$uri"https://graph.microsoft.com/v1.0/users/$Mailbox/messages/$msg/attachments/$att/`$value"
$uri$zip
$zip$ordner
"Geloeschte Elemente"
$Mailbox$msg"deleteditems"
Bei mehr als 100 Mails mit Get-MgUserMessage -All oder Paging arbeiten; für einen Monatslauf reicht meist ein Batch.
7. Report-Mail über Graph senden
Auch Send-MailMessage ist veraltet. Über dieselbe App-Registrierung (Recht Mail.Send) geht die Mail direkt via Graph hinaus – hier mit einer Datei als base64-kodiertem Anhang:
$pfad = "D:\Reports
eport.csv"
$body = @{
message = @{
subject = "eCall Report"
body = @{ contentType = "HTML"; content = "<b>Lauf erfolgreich</b>" }
toRecipients = @(@{ emailAddress = @{ address = "empfaenger@example.com" } })
attachments = @(@{
"@odata.type" = "#microsoft.graph.fileAttachment"
name = Split-Path $pfad -Leaf
contentBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($pfad))
})
}
saveToSentItems = $true
}
Send-MgUserMail -UserId "reporting@example.com" -BodyParameter $body$pfad = "D:\Reports
eport.csv"
$body = @{
message = @{
subject = "eCall Report"
body = @{ contentType = "HTML"; content = "<b>Lauf erfolgreich</b>" }
toRecipients = @(@{ emailAddress = @{ address = "empfaenger@example.com" } })
attachments = @(@{
"@odata.type" = "#microsoft.graph.fileAttachment"
name = Split-Path $pfad -Leaf
contentBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($pfad))
})
}
saveToSentItems = $true
}
Send-MgUserMail -UserId "reporting@example.com" -BodyParameter $body$pfad = "D:\Reports
eport.csv"
$body = @{
message = @{
subject = "eCall Report"
body = @{ contentType = "HTML"; content = "<b>Lauf erfolgreich</b>" }
toRecipients = @(@{ emailAddress = @{ address = "empfaenger@example.com" } })
attachments = @(@{
"@odata.type" = "#microsoft.graph.fileAttachment"
name = Split-Path $pfad -Leaf
contentBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($pfad))
})
}
saveToSentItems = $true
}
Send-MgUserMail -UserId "reporting@example.com" -BodyParameter $body8. Unbeaufsichtigt ausführen
Als geplante Aufgabe läuft das Skript ohne Anmeldung, weil das Zertifikat im Store des Kontos liegt:
$action = New-ScheduledTaskAction -Execute "powershell.exe"
'-NoProfile -ExecutionPolicy Bypass -File "D:\Scripts\graph-import.ps1"'
$trigger
"eCall-Graph-Import"$action$trigger
-User "DOMAIN\svc-ecall" -Password (Read-Host "Passwort"
$action = New-ScheduledTaskAction -Execute "powershell.exe"
'-NoProfile -ExecutionPolicy Bypass -File "D:\Scripts\graph-import.ps1"'
$trigger
"eCall-Graph-Import"$action$trigger
-User "DOMAIN\svc-ecall" -Password (Read-Host "Passwort"
$action = New-ScheduledTaskAction -Execute "powershell.exe"
'-NoProfile -ExecutionPolicy Bypass -File "D:\Scripts\graph-import.ps1"'
$trigger
"eCall-Graph-Import"$action$trigger
-User "DOMAIN\svc-ecall" -Password (Read-Host "Passwort"
Das vollständige Skript – inklusive Logging, Fehlerbehandlung und der eigentlichen Verrechnungslogik – liegt als lauffähiges Beispiel auf GitHub: github.com/pfstr/eCall-Log-Analyzer