Skip to main content

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 personalNotes with SyncId=<guid>;CreatedBy=<upn>. 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: 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

                      Insufficient privileges: admin consent missing or 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

                        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