Exchange Contacts Mirroring & Global Distribution Automation
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".Contacts Itusing createsMicrosoft aGraph single,app-only 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.authentication.
WhyIt this matters:provides:
- One
consistentcentrally maintained contactset for everyone.set. LessAutomaticmanualdistributionmaintenance.back to users.Traceable source using SyncId/CreatedBy stamps.
Functional overview
personalNotes with sync metadata.
Functional Overview
Step 1: User -> Shared mailbox
/users/{id}/contacts).
Writes into shared mailbox folder All Contacts.
Also stamps metadata back to source user contacts when needed.
Step 2: Shared mailbox -> User
Reads shared mailbox contacts only from folder All Contacts. Syncs them into each user's default contacts.Matching and deduplication
Contacts are matched in this order:
SyncId in personalNotes
Primary email (emailAddresses[0].address)
displayName (fallback)
Metadata Stamping (personalNotes)
The script maintains sync tags in personalNotes:
SyncId=<guid>;
CreatedBy=<upnupn-or-id>
LastUpdatedAt=<utc-iso8601>
LastUpdatedBy=<upn-or-id>
Notes behavior:
SyncId is generated if missing.
CreatedBy is preserved from existing synced contact when available.
Update Behavior
UpdateExisting is enabled by default.
An existing target contact is only updated if:
lastModifiedDateTime comparison).
If no change or target is newer/equal, it is skipped.
User Scope / Test Mode
User list resolution:
Flow
$TestUserList/users?$filter=assignedLicenses/...any(x:x/skuId ne null)Prerequisites
Required permissions (Microsoft Graph)
Application permissions (admin consent required):
Contacts.ReadWriteUser.Read.AllDirectory.Read.AllConfiguration
EnvPreferred: varsfull (individualJSON values)config
TheUse script reads:
AUTOMATION_SYNCCONTACTS_TENANT_IDAUTOMATION_SYNCCONTACTS_CONFIG_JSONAUTOMATION_SYNCCONTACTS_CLIENT_IDAUTOMATION_SYNCCONTACTS_CLIENT_SECRETAUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_IDAUTOMATION_SYNCCONTACTS_DRY_RUNExample (PowerShell, session-scoped):
$env:AUTOMATION_SYNCCONTACTS_TENANT_ID ={
"tenant-id-or-domain"MicrosoftTenants": $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",
"TestUserList": ["user1@contoso.com"]
}
],
"DryRun": "true"
}
Test
Multi-tenant mode
via separate env var
You can fillUse $TestUserListAUTOMATION_SYNCCONTACTS_MICROSOFT_TENANTS_JSONinwith thetenant script:array.
Backward-compatible single-tenant env vars
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)
Optional tenant test-user map env var
Works even with full config JSON:
AUTOMATION_SYNCCONTACTS_TEST_USER_LIST_BY_TENANT_JSON
Example:
$TestUserList{
="contoso.onmicrosoft.com": @(
["user1@contoso.com", "user2@contoso.com"],
)"fabrikam.onmicrosoft.com": ["user3@fabrikam.com"]
}
IfLookup thekeys listinclude tenant identifiers like TenantId, PrimaryDomain, TenantDomain.
Prerequisites
Shared mailbox exists and isGlobalAddressBookUserId.
Azure AD app registration with application permissions.
Admin consent granted.
App-only MatchingRequired logicMicrosoft Graph Permissions
EachApplication contact is matched using:
SyncIdpersonalNotesemailAddresses[0].addressdisplayNameAdditional notes:permissions:
SyncIdContacts.ReadWriteis generated if missing.CreatedByUser.Read.Allis taken from existing notesorset to the source user.Directory.Read.All
Dry runRun
Set DryRun / AUTOMATION_SYNCCONTACTS_DRY_RUN to = "true"avoidtrue to simulate actions without writes.
The script logs what it would create/update/skip.be created/updated and prints creation summaries.
CI/CD
Designed for automation pipelines (for example GitLab CI/CD usage
This automation is designed for CI/CD use.CD).
Store required env varssecrets as protected variables in your CI/CD platform.variables.
Troubleshooting
- No tenants configured: set
AUTOMATION_SYNCCONTACTS_CONFIG_JSONorAUTOMATION_SYNCCONTACTS_MICROSOFT_TENANTS_JSON.
Contacts.ReadWriteGraph GlobalAddressBookUserIdSecurity notesNotes
StoreKeeptheClientSecretclientin secure secretsecurely (CI/CD secret, Key Vault, etc.).storage.DocumentRestrict app permissions to minimum required.
PowerShell
Script
SourceThe whole script is available in Github: Automation-ContactsMirroring.ps1