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:
- The header row is optional. If the first line parses as two valid email addresses, it is treated as a mapping, not a header. If either value fails email validation, the row is assumed to be the header and skipped.
- Both addresses on every row must be valid RFC 5322 mailboxes. Invalid rows are reported back in the response and no migration is queued for them.
- Commas inside addresses are not allowed (they never are in practice). There is no quoting support — this is deliberate, not a TODO.
- There is no implicit 1:1 mapping. Every source mailbox needs an explicit destination on its row, even if the addresses are identical.
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
- In Google Cloud Console pick (or create) a project for the migration.
- APIs & Services → Credentials → Create credentials → Service account. Give it a name like
migrationfox-mail. - Skip the optional IAM role assignment — the service account does not need any GCP role. It only needs Workspace delegation.
- On the created account's detail page, open the Keys tab and click Add key → JSON. Save the downloaded JSON.
- Enable the Gmail API on the project (APIs & Services → Library → search "Gmail API" → Enable).
2. Authorize domain-wide delegation
- On the service account detail page copy the Unique ID (the numeric OAuth 2 client ID, 21 digits).
- Open the Google Workspace Admin Console as a super admin.
- Security → Access and data control → API controls → Manage domain-wide delegation.
- Add new, paste the Unique ID, and grant the single scope the migration needs:
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
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
- portal.azure.com → Microsoft Entra ID → App registrations → New registration. Single tenant, no redirect URI needed.
- In the new app, open API permissions → Add a permission → Microsoft Graph → Application permissions. Add
Mail.ReadWrite. Click Grant admin consent for <tenant>. - Open Certificates & secrets → New client secret. Set a 24-month expiry and copy the Value immediately — you cannot see it again after leaving the page.
- Collect the Directory (tenant) ID and Application (client) ID from the Overview tab.
- 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:
- Exist in the destination tenant with the exact UPN in the
destEmailcolumn. - Be licensed with an Exchange Online plan (E1, E3, E5, Business Basic/Standard/Premium, or a standalone Exchange Online Plan 1/2).
- Have a provisioned mailbox. Newly licensed users take several minutes to have a mailbox created — the bulk importer will retry but a row for an unprovisioned user eventually fails with "mailbox not found".
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
- Open Mail Migration in the MigrationFox app and click Bulk import.
- Pick the Gmail service-account source credential and the M365 application destination credential. Both must be saved first.
- Upload the CSV. The wizard validates every row, shows a preview of parsed mappings, and flags invalid addresses before anything is queued.
- 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
- Mail Migration overview — single-mailbox and IMAP/EWS paths
- Common errors — Authentication expired, 403, 429