Skip to content
Back to the blog

Contacts to all mailboxes via PowerShell: what breaks

6 min readPowerShell, Microsoft Graph, Microsoft 365, Contacts, Guide

Auf Deutsch lesen

The request sounds small: “Just write a script that puts the company contacts into all mailboxes.” With Graph PowerShell the first version is done in an afternoon. This post shows that version, and then the eight places where it breaks over the following months. Not to argue against the script, but so that the decision is made with open eyes.

The minimal version

The prerequisite is an app registration in Entra ID with the application permissions Contacts.ReadWrite and User.Read.All, admin consent and a certificate. The script then reads the users and writes each one as a contact into the mailbox of every other:

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
  }
}

That works. Once. With 100 users that is 10,000 write operations, and they land in the default “Contacts” folder of every person, in between the private entries. To avoid that, you first create a dedicated folder with New-MgUserContactFolder and write into it with New-MgUserContactFolderContact. That is where operations begin.

Eight places where it breaks

1. The second run creates duplicates

A contact in Exchange carries no reference to the directory entry it came from. If the script runs again the next day, it creates every contact anew. The solution is a stable key, such as the user’s object ID in an extended property of the contact, and a run that first reads, then compares and writes only the differences. 20 lines become 200.

2. Nobody disappears

When someone leaves the company, their contact stays in all mailboxes. The script needs deletion logic, and that is the most dangerous place: a filter error, a token that expires in the middle of reading, or an empty response, and the comparison reports “all people are missing from the source”. Without a safety threshold the run then empties every folder. Production tools stop at a certain decline and raise the alarm instead of deleting.

3. Throttling

Microsoft Graph answers too many requests with HTTP 429 and a Retry-After header. The minimal version then aborts or writes gaps. Needed are retries with waiting times, batching via $batch (20 requests per call) and a time window in which the run disturbs nobody. With 500 users and 500 contacts that is 250,000 write operations per full run; a run that writes only differences needs a fraction of that after the first time.

4. Partial failures

The script dies at mailbox 143 of 500. What happened to the first 142, what about the rest? A run has to be resumable and know per mailbox where it stood. Otherwise troubleshooting starts from zero, and at night.

5. Secrets and permissions

Client secrets expire after two years at most, usually unnoticed; certificates are better but have to be renewed too. Contacts.ReadWrite as an application permission applies to every mailbox in the tenant. With New-ApplicationAccessPolicy, access can be restricted to a group of mailboxes; that belongs in documentation that someone who did not write the script has to read.

6. Legacy EWS scripts

Many scripts from the last ten years use the EWS Managed API. Microsoft announced in the Message Center that it would block EWS requests from third-party applications to Exchange Online from 1 October 2026 (MC676299, published 21 September 2023, updated 18 April 2025). Anyone running such a script is currently rebuilding it on Microsoft Graph or has had no contact distribution since this month.

7. Nobody notices when it does not run

A scheduled task without a report is only noticed when a new colleague asks after three weeks why nobody has them in their phone. What is needed is a run that reports: counts, errors, duration, and that also when it did not start at all.

8. Operations and staff turnover

Where does the script run, on which server, under which account, who renews the certificate, who understands the code after the author has left? A script is finished after an afternoon; an operations manual, monitoring and a deputy are not.

When a script is enough

  • A one-off import, say when moving from an old system, with no ongoing maintenance.
  • Few users, few changes, one person who knows the script and is reachable.
  • One source, one set of recipients, no deletions: then creating is all that is needed.

Daily, with leavers, with several sources or with recipient groups that differ: then the script is a product that someone has to operate.

The other side of the calculation

GALYNSKI is exactly this operation as a service: admin consent instead of certificate maintenance, settings instead of code, a nightly run that writes only differences, protection against mass deletion, reports to the administrators and a clean-up when it is over. The sources are the global address list, groups, the contact folder of a shared mailbox, SharePoint lists and external contacts. 4 € per user per month, less on the annual plan.

Checklist, if it is going to be the script after all

  1. A dedicated contact folder per mailbox, never the default folder.
  2. A stable key per contact; read, compare, write only differences.
  3. Deletion threshold: stop and report at a decline of more than 20 per cent.
  4. Handle 429 with Retry-After, use $batch, run at night.
  5. Resumable per mailbox, log errors per mailbox.
  6. Certificate instead of secret, expiry in the calendar, application access policy on the group concerned.
  7. Report after every run to two people, alarm when a run fails to start.
  8. Operations manual with server, account, permissions, deputy.

Frequently asked questions

Which permission does a script need to write contacts into other people’s mailboxes?

The application permission Contacts.ReadWrite with admin consent, plus User.Read.All for the GAL. It applies to all mailboxes of the tenant. With an application access policy in Exchange Online it can be restricted to a group of mailboxes.

Why does the second run create duplicates?

Because a contact carries no key that links it to the directory entry. A script that only creates will create everything again on the second run. The script has to remember which contact it wrote for which person and update instead of create on the next run.

What changed on 1 October 2026?

Since that date Microsoft has been gradually blocking EWS requests from third-party applications to Exchange Online (Message Center MC676299). Scripts based on the EWS Managed API or on EWS modules have to be moved to Microsoft Graph.

Read next