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.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.StampingTraceability inpersonalNoteswith sync metadata.
Functional Overview
Step 1: User -> Shared mailbox
- Reads each user's default contacts (
/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
Matching and deduplication
Contacts are matched in this order:
SyncIdinpersonalNotes- 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:
- Existing non-sync note text is preserved.
- Old sync tags are replaced with fresh values.
SyncIdis generated if missing.CreatedByis preserved from existing synced contact when available.
Update Behavior
UpdateExisting is enabled by default.
An existing target contact is only updated if:
- Data has changed, and
- Target contact is older than source (
lastModifiedDateTimecomparison).
If no change or target is newer/equal, it is skipped.
User Scope / Test Mode
User list resolution:
- Per-tenant test users (if configured).
Optional test mode via a local test user list inside the script.Build the user list:If$TestUserListis populated: only those UPNs.Otherwise:Otherwise all licensed users fromGraphGraph:(/users?$filter=assignedLicenses/...any(x:x/skuId ne null)).
Ensure the "All Contacts" folder exists in the shared mailbox.Sync step 1: user -> shared mailbox.Sync step 2: shared mailbox -> user.
Contacts.ReadWrite(read/write contacts infrom userandsyncsharedloops.mailboxes).User.Read.AllorDirectory.Read.All(list licensed users).AUTOMATION_SYNCCONTACTS_TENANT_IDAUTOMATION_SYNCCONTACTS_CONFIG_JSONAUTOMATION_SYNCCONTACTS_CLIENT_IDAUTOMATION_SYNCCONTACTS_CLIENT_SECRETAUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_IDAUTOMATION_SYNCCONTACTS_DRY_RUN(true/false)AUTOMATION_SYNCCONTACTS_TENANT_IDAUTOMATION_SYNCCONTACTS_CLIENT_IDAUTOMATION_SYNCCONTACTS_CLIENT_SECRETAUTOMATION_SYNCCONTACTS_GLOBAL_ADDRESS_BOOK_USER_IDAUTOMATION_SYNCCONTACTS_DRY_RUN(true/false)AUTOMATION_SYNCCONTACTS_TEST_USER_LIST_BY_TENANT_JSONSyncIdinpersonalNotesPrimary email (emailAddresses[0].address)displayNameSyncIdContacts.ReadWriteis generated if missing.CreatedByUser.Read.Allis taken from existing notesorset to the source user.Directory.Read.All- No tenants configured: set
AUTOMATION_SYNCCONTACTS_CONFIG_JSONorAUTOMATION_SYNCCONTACTS_MICROSOFT_TENANTS_JSON. - Insufficient privileges: missing admin consent
missingorGraphContacts.ReadWritemissing.permissions. - Shared mailbox not found: incorrect
GlobalAddressBookUserIdis wrong or mailbox does not exist.. MissingNoSyncId/CreatedBylicensed users found:existingtenantcontactshaswithoutnonotes;userswillmatchingbelicensestampedfilter.- Config
nextJSONrun.parse errors: invalid JSON in env vars. StoreKeeptheClientSecretclientin secure secretsecurely (CI/CD secret, Key Vault, etc.).storage.DocumentRestrict app permissions to minimum required.- Limit and audit access to
thesharedmailbox.mailbox Grantcontactonly the minimum necessary app permissions.data.
Flow
Prerequisites
Required permissions (Microsoft Graph)
Application permissions (admin consent required):
Configuration
EnvPreferred: varsfull (individualJSON values)config
TheUse script reads:
Example (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"
}
TestMulti-tenant mode
via separate env var
You can fillUse $TestUserListAUTOMATION_SYNCCONTACTS_MICROSOFT_TENANTS_JSONinwith thetenant script:array.
Backward-compatible single-tenant env vars
Optional tenant test-user map env var
Works even with full config 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
MatchingRequired logicMicrosoft Graph Permissions
EachApplication contact is matched using:
Additional notes:permissions:
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
Security notesNotes
PowerShell
Script
Source
The whole script is available in Github: Automation-ContactsMirroring.ps1