← Back to Blog

MIGRATION · April 14, 2026 · 7 min read

Bulk mail migration with user mapping

Single-user mail migration is a solved problem. You OAuth into the source, OAuth into the destination, click Start, walk away. Bulk migration — 50 users, 500 users, the whole @acme.com domain flipping to @newparent.com after an acquisition closes — is a different animal. Two things break as soon as you move past one mailbox: the UPNs do not line up, and the per-user OAuth model stops scaling. This post covers how to solve both.

The UPN-mismatch problem

In a simple tenant-to-tenant migration where both sides own the same primary domain (acme.com on Gmail, acme.com on Microsoft 365), the mailboxes line up by themselves. alice@acme.com on the source goes to alice@acme.com on the destination. Nothing special is needed.

In practice, that is almost never the situation. Here is what real migrations look like:

In each case you need a translation layer: a source UPN comes in, a destination UPN goes out. That layer is the mapping CSV.

The CSV format

MigrationFox’s bulk mail migration expects a two-column CSV with a header row:

source_email,destination_upn
alice@acme.com,alice.smith@newparent.com
bob@acme.com,bob.jones@newparent.com
shared-support@acme.com,support@newparent.com
finance-team@acme.com,finance@newparent.com

One row per user. The source email must be a valid Gmail address that the Workspace service account can impersonate via domain-wide delegation. The destination UPN must be an existing, licensed, mailbox-enabled Microsoft 365 user.

A few practical notes on the CSV:

Service account vs OAuth: why bulk requires a service account

The per-user OAuth model — where each mailbox owner clicks through a Google consent screen to grant access — does not scale past a handful of users. For three mailboxes, OAuth each one. For 300, you need domain-wide delegation.

Here is the difference:

OAuth (per-user consent)

Service account with domain-wide delegation

Setting it up is a one-time step. In the Google Cloud Console you create a service account, generate a JSON key, note the OAuth 2 client ID, and paste that client ID into the Workspace admin console under Security → API controls → Domain-wide delegation with the Gmail read scope:

https://www.googleapis.com/auth/gmail.readonly

From then on, MigrationFox uses the service account to impersonate each mailbox listed in the CSV, one at a time, with no user interaction.

App-only on the destination side

The destination requires the same kind of thinking. You do not want to OAuth into each Microsoft 365 user individually, and you do not want the migration to break when the service-principal token expires mid-job.

The Microsoft Graph equivalent is an app-only permission — specifically:

Application permissions authenticate with the client-credentials flow (client ID + client secret, or a certificate) and write into any mailbox in the tenant without impersonating a user. A tenant admin grants consent once, and the migration runs unattended.

If your security team does not like “any mailbox in the tenant”, you can scope the app with a Graph Application Access Policy that restricts it to a specific mail-enabled security group. The policy is configured in PowerShell (New-ApplicationAccessPolicy); your migration still works, and the app cannot touch mailboxes outside the allowed group.

A worked example

Acme Inc. is being absorbed by NewParent Corp. The migration is 127 mailboxes over a single weekend. Here is how it lines up end to end.

1. Pre-migration provisioning

NewParent’s IT team provisions 127 destination mailboxes, one per Acme user, under the newparent.com primary domain. Each new user gets a licence (Business Standard), a first sign-in to force mailbox creation, and a forwarding rule in the NewParent tenant that is disabled until cutover.

2. Google service account

In the Acme Google Cloud org, someone with super-admin rights creates a service account migrationfox@acme-migration.iam.gserviceaccount.com, generates a JSON key, and authorises the service account’s client ID in the Workspace admin console with gmail.readonly.

3. Azure AD app registration

In the NewParent tenant, an admin creates an app registration “MigrationFox Mail Ingest”, adds the Graph Mail.ReadWrite application permission, generates a client secret, and grants admin consent. A Graph Application Access Policy is added to restrict the app to the security group acme-migration-destinations which contains the 127 destination UPNs.

4. Mapping CSV

HR hands IT a spreadsheet with the old-domain-to-new-domain mapping for every Acme employee who will be retained. IT saves it as acme-to-newparent.csv:

source_email,destination_upn
alice.smith@acme.com,alice.smith@newparent.com
bob.jones@acme.com,bob.jones@newparent.com
... (125 more rows)

5. MigrationFox job

IT logs in to app.migrationfox.com, creates a Mail Migration job, pastes the Google service account JSON and the Azure AD app credentials, uploads the CSV, and runs a pre-flight. The pre-flight confirms all 127 source mailboxes are reachable and all 127 destination UPNs are provisioned. One row flagged — carol.lee@acme.com maps to carol.lee@newparent.com but that destination user was not provisioned yet. IT fixes it and re-runs the pre-flight. All green.

6. Cutover

Friday 6pm, the Acme MX record is updated to point at NewParent’s Microsoft 365 tenant. Inbound mail now arrives at the new destination. The MigrationFox job starts ingesting historical Gmail content — folders, labels-as-folders, attachments, everything — in parallel across the 127 mailboxes.

7. Monday morning

Migration is 94% done at 8am Monday. The longest tail is three legacy mailboxes with 40+ GB of 2015–2019 archive content. By noon, 100%. The job report shows 127 of 127 mailboxes completed clean, zero verification failures, ~4.2 million messages migrated. IT archives the report for the audit file.

The single most important sentence in this post: every destination UPN in the CSV must exist as a licensed, mailbox-enabled user in Microsoft 365 before you start the job. The pre-flight will catch missing ones, but you cannot migrate into a mailbox that does not exist.

Related reading

Get started

Create a free account at app.migrationfox.com/register, run a pre-flight on 5 mailboxes at no cost, and scale up when the numbers line up.

Start a bulk mail migration

Free pre-flight on your first 5 mailboxes.

Start Free →