Skip to main content

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 contact set for everyone.set.
  • LessAutomatic manualdistribution maintenance.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 in personalNotes with 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

        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:

              Existing non-sync note text is preserved. Old sync tags are replaced with fresh values. 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:

                Data has changed, and Target contact is older than source (lastModifiedDateTime comparison).

                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.

                  Flow

                    Build the user list:
                      If $TestUserList is populated: only those UPNs. Otherwise:Otherwise all licensed users from GraphGraph: (
                        /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.

                        Prerequisites

                          Shared mailbox availableaccount (GlobalAddressBookUserId).

                          is Appexcluded 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 infrom user andsync sharedloops. mailboxes). User.Read.All or Directory.Read.All (list licensed users).

                            Configuration

                            EnvPreferred: varsfull (individualJSON values)config

                            TheUse script reads:

                              AUTOMATION_SYNCCONTACTS_TENANT_IDAUTOMATION_SYNCCONTACTS_CONFIG_JSON 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"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_JSON inwith 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 is notreachable empty,as GlobalAddressBookUserId. Azure AD app registration with application permissions. Admin consent granted. App-only theseaccess usersto aremailbox synchronized.contacts. This makes safe testing easy.

                                    MatchingRequired logicMicrosoft Graph Permissions

                                    EachApplication contact is matched using:

                                      SyncId in personalNotes Primary email (emailAddresses[0].address) displayName

                                      Additional notes:permissions:

                                      • SyncIdContacts.ReadWrite is generated if missing.
                                      • CreatedByUser.Read.All is taken from existing notes or set to the source user.Directory.Read.All

                                      Dry runRun

                                      Set DryRun / AUTOMATION_SYNCCONTACTS_DRY_RUN = "true" to 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_JSON or AUTOMATION_SYNCCONTACTS_MICROSOFT_TENANTS_JSON.
                                      Insufficient privileges: missing admin consent missing or Contacts.ReadWriteGraph missing.permissions. Shared mailbox not found: incorrect GlobalAddressBookUserId is wrong or mailbox does not exist.. MissingNo SyncId/CreatedBylicensed users found: existingtenant contactshas withoutno notes;users willmatching belicense stampedfilter. onConfig nextJSON run.parse errors: invalid JSON in env vars.

                                      Security notesNotes

                                      • StoreKeep theClientSecret clientin secure secret securely (CI/CD secret, Key Vault, etc.).storage.
                                      • DocumentRestrict app permissions to minimum required.
                                      Limit and audit access to the shared mailbox.mailbox Grantcontact only the minimum necessary app permissions.data.

                                      PowerShell

                                      Script

                                       Source

                                      The whole script is available in Github: Automation-ContactsMirroring.ps1