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:
- One consistent contact set for everyone.
- Less manual maintenance.
- Traceable source using SyncId/CreatedBy stamps.
- Secure app-only access without user login.
Functional overview
- Step 1: Each user's "Your contacts" -> shared mailbox folder "All Contacts".
- Step 2: Shared mailbox folder "All Contacts" -> each user's "Your contacts".
- Deduplication and matching using SyncId + primary email + displayName.
- Stamping in
personalNoteswithSyncId=<guid>;CreatedBy=<upn>. - Optional test mode via a local test user list inside the script.
Flow
- Build the user list:
- If
$TestUserListis populated: only those UPNs. - Otherwise: all licensed users from Graph (
/users?$filter=assignedLicenses/...).
- If
- Ensure the "All Contacts" folder exists in the shared mailbox.
- Sync step 1: user -> shared mailbox.
- Sync step 2: shared mailbox -> user.
Prerequisites
Required permissions (Microsoft Graph)
Application permissions (admin consent required):
Contacts.ReadWrite(read/write contacts in user and shared mailboxes).User.Read.AllorDirectory.Read.All(list licensed users).
Configuration
Env vars (individual values)
The script reads:
AUTOMATION_SYNCCONTACTS_TENANT_IDAUTOMATION_SYNCCONTACTS_CLIENT_IDAUTOMATION_SYNCCONTACTS_CLIENT_SECRETAUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_IDAUTOMATION_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:
SyncIdinpersonalNotes- Primary email (
emailAddresses[0].address) displayName
Additional notes:
SyncIdis generated if missing.CreatedByis 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
- Insufficient privileges: admin consent missing or
Contacts.ReadWritemissing. - Shared mailbox not found:
GlobalAddressBookUserIdis wrong or mailbox does not exist. - Missing SyncId/CreatedBy: existing contacts without notes; will be stamped on next run.
Security notes
- Store the client secret securely (CI/CD secret, Key Vault, etc.).
- Document access to the shared mailbox.
- Grant only the minimum necessary app permissions.
PowerShell Script
The whole script is available in Github: Automation-ContactsMirroring.ps1