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:

  1. SyncId in personalNotes
  2. Primary email (emailAddresses[0].address)
  3. 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:

  1. Per-tenant test users (if configured).
  2. Optional test mode via a local test user list inside the script.
  3. Flow

    1. 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:

    1. SyncId in personalNotes
    2. Primary email (emailAddresses[0].address)
    3. 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.
    • on
    • Config 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