Kontakte per PowerShell in alle Postfächer: Was im Betrieb bricht
Der Auftrag klingt klein: „Schreib doch ein Skript, das die Firmenkontakte in alle Postfächer legt.“ Mit Graph PowerShell ist die erste Fassung an einem Nachmittag fertig. Dieser Beitrag zeigt diese Fassung, und dann die acht Stellen, an denen sie in den folgenden Monaten bricht. Nicht, um vom Skript abzuraten, sondern damit die Entscheidung mit offenen Augen fällt.
Die Minimalfassung
Voraussetzung ist eine App-Registrierung in Entra ID mit den Anwendungsberechtigungen Contacts.ReadWrite und User.Read.All, Admin-Consent und einem Zertifikat. Dann liest das Skript die Benutzer und schreibt jeden als Kontakt in das Postfach jedes anderen:
Connect-MgGraph -ClientId $appId -TenantId $tenantId -CertificateThumbprint $thumb
$users = Get-MgUser -All -ConsistencyLevel eventual -CountVariable n `
-Filter "accountEnabled eq true and mail ne null" `
-Property id,displayName,mail,mobilePhone,businessPhones,jobTitle,companyName
foreach ($target in $users) {
foreach ($u in $users) {
New-MgUserContact -UserId $target.Id -BodyParameter @{
displayName = $u.DisplayName
emailAddresses = @(@{ address = $u.Mail; name = $u.DisplayName })
mobilePhone = $u.MobilePhone
businessPhones = $u.BusinessPhones
jobTitle = $u.JobTitle
companyName = $u.CompanyName
} | Out-Null
}
}Das funktioniert. Einmal. Bei 100 Benutzern sind das 10.000 Schreibvorgänge, und sie landen im Standardordner „Kontakte“ jeder Person, zwischen den privaten Einträgen. Wer das vermeiden will, legt mit New-MgUserContactFolder zuerst einen eigenen Ordner an und schreibt mit New-MgUserContactFolderContact hinein. Damit beginnt der Betrieb.
Acht Stellen, an denen es bricht
1. Der zweite Lauf erzeugt Duplikate
Ein Kontakt in Exchange trägt keinen Verweis auf den Verzeichniseintrag, aus dem er entstand. Läuft das Skript am nächsten Tag wieder, legt es jeden Kontakt erneut an. Die Lösung ist ein stabiler Schlüssel, etwa die Objekt-ID des Benutzers in einer Erweiterungseigenschaft des Kontakts, und ein Lauf, der zuerst liest, dann vergleicht und nur Unterschiede schreibt. Aus 20 Zeilen werden 200.
2. Niemand verschwindet
Verlässt jemand die Firma, bleibt sein Kontakt in allen Postfächern. Das Skript braucht eine Löschlogik, und die ist die gefährlichste Stelle: Ein Filterfehler, ein abgelaufenes Token mitten im Lesen oder eine leere Antwort, und der Vergleich meldet „alle Personen fehlen in der Quelle“. Ohne Schutzschwelle räumt der Lauf dann jeden Ordner leer. Produktive Werkzeuge halten ab einem bestimmten Rückgang an und melden sich, statt zu löschen.
3. Drosselung
Microsoft Graph antwortet bei zu vielen Anfragen mit HTTP 429 und einem Retry-After-Header. Die Minimalfassung bricht dann ab oder schreibt Lücken. Nötig sind Wiederholungen mit Wartezeit, Bündelung über $batch (20 Anfragen je Aufruf) und ein Zeitfenster, in dem der Lauf niemanden stört. Bei 500 Benutzern und 500 Kontakten sind es 250.000 Schreibvorgänge je Volllauf; ein Lauf, der nur Unterschiede schreibt, braucht nach dem ersten Mal nur einen Bruchteil davon.
4. Teilausfälle
Das Skript stirbt bei Postfach 143 von 500. Was ist mit den ersten 142 passiert, was mit dem Rest? Ein Lauf muss wiederaufnehmbar sein und je Postfach wissen, wo er stand. Sonst beginnt die Fehlersuche bei null, und zwar nachts.
5. Secrets und Berechtigungen
Client Secrets laufen nach höchstens zwei Jahren ab, meist unbemerkt; Zertifikate sind besser, müssen aber ebenfalls erneuert werden. Contacts.ReadWrite als Anwendungsberechtigung gilt für jedes Postfach im Tenant. Mit New-ApplicationAccessPolicy lässt sich der Zugriff auf eine Gruppe von Postfächern einschränken; das gehört in die Dokumentation, die jemand lesen muss, der das Skript nicht geschrieben hat.
6. EWS-Altskripte
Viele Skripte aus den letzten zehn Jahren nutzen die EWS Managed API. Microsoft hat im Message Center angekündigt, EWS-Anfragen von Drittanwendungen an Exchange Online ab dem 1. Oktober 2026 zu blockieren (MC676299, veröffentlicht 21. September 2023, aktualisiert 18. April 2025). Wer so ein Skript betreibt, baut es gerade auf Microsoft Graph um oder hat seit diesem Monat keine Kontaktverteilung mehr.
7. Keiner merkt, wenn es nicht läuft
Ein geplanter Task ohne Bericht fällt erst auf, wenn ein neuer Kollege nach drei Wochen fragt, warum ihn niemand im Telefon hat. Nötig ist ein Lauf, der sich meldet: Anzahl, Fehler, Dauer, und zwar auch dann, wenn er gar nicht gestartet ist.
8. Betrieb und Personalwechsel
Wo läuft das Skript, auf welchem Server, unter welchem Konto, wer erneuert das Zertifikat, wer versteht den Code nach dem Weggang der Autorin? Ein Skript ist nach einem Nachmittag fertig; eine Betriebsdoku, ein Monitoring und eine Vertretung nicht.
Wann ein Skript reicht
- Einmaliger Import, etwa beim Umzug von einem alten System, ohne laufende Pflege.
- Wenige Benutzer, wenige Änderungen, eine Person, die das Skript kennt und erreichbar ist.
- Eine Quelle, ein Ziel, keine Löschungen: Dann ist Anlegen alles, was gebraucht wird.
Täglich, mit Austritten, mit mehreren Quellen oder mit Zielgruppen, die sich unterscheiden: Dann ist das Skript ein Produkt, das jemand betreiben muss.
Die andere Seite der Rechnung
GALYNSKI ist genau dieser Betrieb als Dienst: Admin-Consent statt Zertifikatspflege, Regeln statt Code, ein nächtlicher Lauf, der nur Unterschiede schreibt, ein Schutz vor Massenlöschung, Berichte an die Administratoren und ein Rückbau, wenn es vorbei ist. Als Quellen gibt es die globale Adressliste, Gruppen, den Kontaktordner eines freigegebenen Postfachs und SharePoint-Listen. 4 € je Benutzer und Monat, im Jahresabo weniger.
Checkliste, falls es doch das Skript wird
- Eigener Kontaktordner je Postfach, nie der Standardordner.
- Stabiler Schlüssel je Kontakt; lesen, vergleichen, nur Unterschiede schreiben.
- Löschschwelle: ab einem Rückgang von mehr als 20 Prozent anhalten und melden.
- 429 mit Retry-After behandeln, $batch nutzen, nachts laufen.
- Je Postfach wiederaufnehmbar, Fehler je Postfach protokollieren.
- Zertifikat statt Secret, Ablauf im Kalender, Application Access Policy auf die Zielgruppe.
- Bericht nach jedem Lauf an zwei Personen, Alarm bei ausgefallenem Lauf.
- Betriebsdoku mit Server, Konto, Berechtigungen, Vertretung.
Häufige Fragen
Welche Berechtigung braucht ein Skript, das Kontakte in fremde Postfächer schreibt?
Die Anwendungsberechtigung Contacts.ReadWrite mit Admin-Consent, für die GAL zusätzlich User.Read.All. Sie gilt für alle Postfächer des Tenants. Mit einer Application Access Policy in Exchange Online lässt sie sich auf eine Gruppe von Postfächern einschränken.
Warum entstehen beim zweiten Lauf Duplikate?
Weil ein Kontakt keinen Schlüssel trägt, der ihn mit dem Verzeichniseintrag verbindet. Wer nur anlegt, legt beim zweiten Lauf alles noch einmal an. Das Skript muss sich merken, welchen Kontakt es für welche Person geschrieben hat, und beim nächsten Lauf aktualisieren statt anlegen.
Was hat sich am 1. Oktober 2026 geändert?
Microsoft blockiert seit diesem Datum schrittweise EWS-Anfragen von Drittanwendungen an Exchange Online (Message Center MC676299). Skripte auf Basis der EWS Managed API oder von EWS-Modulen müssen auf Microsoft Graph umgestellt werden.