🦊 MigrationFox Docs

Bulk Mail Migration with User Mapping

Migrate an entire organization's mailboxes in one CSV upload. Currently scoped to Gmail → Microsoft 365 Exchange Online; the IMAP and EWS paths remain one-at-a-time.

CSV Format

The bulk endpoint accepts a plain CSV with two columns. Each row queues one source mailbox → destination mailbox migration:

sourceEmail,destEmail
jane.doe@source-domain.com,jane.doe@dest-domain.com
raj.patel@source-domain.com,raj.patel@dest-domain.com
finance-archive@source-domain.com,finance@dest-domain.com

Rules:

Address renaming is a migration boundary

The left column is where the source tenant looks for mail. The right column is the destination mailbox the messages land in — it is not a forwarding alias and not an SMTP "Mail From" rewrite. Use the destination's actual primary SMTP address. If you map to a non-existent destination, the bulk run queues the job, the worker picks it up, and the per-row job fails with a "mailbox not found" error visible on the jobs page.

Source Requirements — Gmail

Bulk mode requires a Google service account with domain-wide delegation. Per-user OAuth does not work for bulk — each OAuth grant is tied to a single user and there is no way to impersonate the rest of the organization from one consent. The service account is the mechanism by which one set of credentials reads every user's mail.

1. Create the service account

  1. In Google Cloud Console pick (or create) a project for the migration.
  2. APIs & ServicesCredentialsCreate credentialsService account. Give it a name like migrationfox-mail.
  3. Skip the optional IAM role assignment — the service account does not need any GCP role. It only needs Workspace delegation.
  4. On the created account's detail page, open the Keys tab and click Add keyJSON. Save the downloaded JSON.
  5. Enable the Gmail API on the project (APIs & ServicesLibrary → search "Gmail API" → Enable).

2. Authorize domain-wide delegation

  1. On the service account detail page copy the Unique ID (the numeric OAuth 2 client ID, 21 digits).
  2. Open the Google Workspace Admin Console as a super admin.
  3. SecurityAccess and data controlAPI controlsManage domain-wide delegation.
  4. Add new, paste the Unique ID, and grant the single scope the migration needs:
https://www.googleapis.com/auth/gmail.readonly

Read-only is sufficient — the migration never writes to Gmail. If you need to export labels alongside messages, add https://www.googleapis.com/auth/gmail.metadata as well.

3. Upload the service account JSON in MigrationFox

In the mail migration wizard, select Gmail (service account) as the source, paste or upload the JSON key file, and confirm the source domain. The wizard calls the Gmail API with a synthetic user (impersonate = the first destination row in your CSV) to verify the delegation took effect before accepting the CSV.

OAuth won't work for bulk

The individual Gmail — OAuth flow is fine for single-mailbox migrations, but a bulk run against that credential will fail on the second mailbox with an impersonation error. Use the service-account path or run the mailboxes one at a time.

Destination Requirements — Microsoft 365

The destination side needs an Entra ID app registration with application (not delegated) permissions. The migration worker connects as the app identity and writes into each destination mailbox by UPN. There is no per-user OAuth prompt.

Required application permission

Mail.ReadWrite

Critically, this must be the Application permission, not the Delegated one. The delegated version of the same scope authorizes only the signed-in user's own mailbox — the bulk importer will fail the first cross-user write with a 403.

App registration steps

  1. portal.azure.comMicrosoft Entra IDApp registrationsNew registration. Single tenant, no redirect URI needed.
  2. In the new app, open API permissionsAdd a permissionMicrosoft GraphApplication permissions. Add Mail.ReadWrite. Click Grant admin consent for <tenant>.
  3. Open Certificates & secretsNew client secret. Set a 24-month expiry and copy the Value immediately — you cannot see it again after leaving the page.
  4. Collect the Directory (tenant) ID and Application (client) ID from the Overview tab.
  5. In MigrationFox, pick Microsoft 365 Mail (application) as the destination and paste tenant ID, client ID, and client secret.

Optional: scope the app to specific mailboxes

Global Mail.ReadWrite lets the app write to every mailbox in the tenant. To restrict it to the mailboxes in your CSV, configure an Application Access Policy via Exchange Online PowerShell (New-ApplicationAccessPolicy) that binds the app to a mail-enabled security group containing just the target users. Do this after running a one-mailbox smoke test so you are sure the credential works first.

Per-user destination prerequisites

For each row in the CSV, the destination user must already:

A good pre-flight is to run Get-Mailbox -Identity <destEmail> in Exchange Online PowerShell for every row before uploading the CSV. Any UPN that returns no result should be licensed and re-verified.

Running a Bulk Migration

  1. Open Mail Migration in the MigrationFox app and click Bulk import.
  2. Pick the Gmail service-account source credential and the M365 application destination credential. Both must be saved first.
  3. Upload the CSV. The wizard validates every row, shows a preview of parsed mappings, and flags invalid addresses before anything is queued.
  4. Click Start. One migration job is created per row and the dashboard shows per-mailbox progress, message counts, and attachment sizes.

Jobs run in parallel up to the project's concurrency setting. A row that fails (licensing gap, mailbox not found, transient Graph 429) does not halt the others — the failed row can be retried individually from the jobs page.

Related