Exchange Contacts Mirroring & Global Distribution Automation
Summary and reason
This automation synchronizes contacts between all licensed user mailboxes and a central shared mailbox folder named "All Contacts". It creates a single, maintained contact source while still making the contacts available inside each user's mailbox. The goal is central maintenance (shared mailbox) without manual copying into every mailbox.
Why this matters:
Functional overview
personalNotes with SyncId=<guid>;CreatedBy=<upn>.
Optional test mode via a local test user list inside the script.
Flow
$TestUserList is populated: only those UPNs.
Otherwise: all licensed users from Graph (/users?$filter=assignedLicenses/...).
Ensure the "All Contacts" folder exists in the shared mailbox.
Sync step 1: user -> shared mailbox.
Sync step 2: shared mailbox -> user.
Prerequisites
Shared mailbox available (GlobalAddressBookUserId). App registration with application permissions. Admin consent granted for the app. App-only access to all mailbox contacts.Required permissions (Microsoft Graph)
Application permissions (admin consent required):
Contacts.ReadWrite (read/write contacts in user and shared mailboxes).
User.Read.All or Directory.Read.All (list licensed users).
Configuration
Env vars (individual values)
The script reads:
AUTOMATION_SYNCCONTACTS_TENANT_ID
AUTOMATION_SYNCCONTACTS_CLIENT_ID
AUTOMATION_SYNCCONTACTS_CLIENT_SECRET
AUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_ID
AUTOMATION_SYNCCONTACTS_DRY_RUN (true/false)
Example (PowerShell, session-scoped):
$env:AUTOMATION_SYNCCONTACTS_TENANT_ID = "tenant-id-or-domain"
$env:AUTOMATION_SYNCCONTACTS_CLIENT_ID = "app-client-id"
$env:AUTOMATION_SYNCCONTACTS_CLIENT_SECRET = "app-client-secret"
$env:AUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_ID = "shared.contacts@contoso.com"
$env:AUTOMATION_SYNCCONTACTS_DRY_RUN = "true"
Env var as JSON (optional)
If AUTOMATION_SYNCCONTACTS_CONFIG_JSON is set, it is preferred:
{
"TenantId": "tenant-id-or-domain",
"ClientId": "app-client-id",
"ClientSecret": "app-client-secret",
"GlobalAddressBookUserId": "shared.contacts@contoso.com",
"DryRun": "true"
}
Test mode
You can fill $TestUserList in the script:
$TestUserList = @(
"user1@contoso.com",
"user2@contoso.com"
)
If the list is not empty, only these users are synchronized. This makes safe testing easy.
Matching logic
Each contact is matched using:
SyncId in personalNotes
Primary email (emailAddresses[0].address)
displayName
Additional notes:
SyncId is generated if missing.
CreatedBy is taken from existing notes or set to the source user.
Dry run
Set AUTOMATION_SYNCCONTACTS_DRY_RUN = "true" to avoid writes. The script logs what it would create/update/skip.
GitLab CI/CD usage
This automation is designed for CI/CD use. Store required env vars as protected variables in your CI/CD platform.
Troubleshooting
Contacts.ReadWrite missing.
Shared mailbox not found: GlobalAddressBookUserId is wrong or mailbox does not exist.
Missing SyncId/CreatedBy: existing contacts without notes; will be stamped on next run.
Security notes
PowerShell Script
The whole script is available in Github: Automation-ContactsMirroring.ps1