PLATFORM · GMAIL → M365
Bulk-move Gmail to Microsoft 365 — with UPN mapping.
Most Gmail-to-Microsoft-365 migrations are not one mailbox; they are 50, or 500, or the entire @oldcompany.com domain cutting over to @newcompany.com the weekend after an acquisition closes. MigrationFox handles that case natively: one Google service account on the source, one app-only Azure AD app on the destination, and a CSV in the middle that tells the engine which source user maps to which destination UPN.
What Gets Migrated
- Folder structure preserved. Gmail’s system folders (
Inbox,Sent,Drafts,Archive) land in the corresponding Microsoft 365 Well-Known folders. Nothing is dumped into a flat “Imported” folder. - Labels become folders. Gmail user labels are recreated as folders under the destination mailbox root. Nested labels (
Clients/Acme/Contracts) become nested folders. A message with multiple labels is copied to each corresponding folder, matching Gmail’s own display semantics. - Attachments. Full attachment fidelity up to the destination’s mailbox limit (Microsoft 365 defaults to 150 MB per message). Large attachments are streamed, not buffered in memory on the worker.
- Message metadata. Sent date, received date, from, to, cc, bcc, subject, and the raw MIME body are preserved. Read/unread status transfers. Message-ID is retained where Graph accepts it, preserving reply-threading in the destination.
- Per-user folder filtering. Skip
Spam,Trash, or any custom labels you do not want to carry forward. Applied per row of the mapping CSV.
Source: Google Workspace Service Account + Domain-Wide Delegation
Bulk mail migration is only practical when the tool can impersonate every source mailbox without an end-user OAuth prompt for each one. That is what domain-wide delegation is for.
You create a service account in Google Cloud, generate a JSON key, and authorise the service account’s client ID in the Google Workspace admin console with the Gmail read scope:
https://www.googleapis.com/auth/gmail.readonly
Once authorised, MigrationFox uses the service account to mint a short-lived access token for each source mailbox on demand (sub-scoped to the user’s primary email). The token is never written to disk and is discarded when the mailbox finishes migrating. The JSON key itself is stored encrypted at rest with AES-256-GCM.
Destination: App-Only Microsoft 365 with Mail.ReadWrite Application
On the Microsoft 365 side you register an Azure AD app with the Microsoft Graph application permission (not delegated):
Mail.ReadWrite(Application)
A tenant admin grants consent. MigrationFox then authenticates with the client credentials flow and writes into any mailbox in the tenant via /users/{upn}/mailFolders/.../messages. No per-user OAuth consent is needed and no MFA prompts block the migration at 3am on a Sunday.
If your security team prefers it, scope the app with a Graph Application Access Policy that restricts it to a specific mail-enabled security group. The migration still works; the blast radius is bounded to the mailboxes you intend to touch.
The UPN Mapping CSV
The mapping CSV is where a bulk migration becomes actually bulk. Two columns, one row per user:
source_email,destination_upn
alice@oldcompany.com,alice.smith@newcompany.com
bob@oldcompany.com,bob.jones@newcompany.com
shared-support@oldcompany.com,support@newcompany.com
Each row becomes a unit of work. The engine processes rows in parallel up to your plan’s concurrency limit, and every row produces its own line in the job report: emails migrated, folders created, attachments transferred, errors encountered. You can re-run a single failed row without re-running the whole batch.
Shared mailboxes and group aliases work the same way — Google’s service account can impersonate them if they are Workspace mailboxes, and the destination just needs to be a mailbox-enabled recipient. Resource mailboxes (rooms, equipment) are out of scope — they are not Gmail concepts on the source side.
Requirements Checklist
Before you start, have these ready:
- A Google Cloud project with the Gmail API enabled
- A service account in that project with a generated JSON key
- The service account’s OAuth 2 client ID authorised in Google Workspace admin (Security → API controls → Domain-wide delegation) with the
gmail.readonlyscope - An Azure AD app registration with Microsoft Graph
Mail.ReadWriteapplication permission and admin consent granted - The destination UPNs already provisioned as mailboxes in Microsoft 365 (licence assigned, mailbox enabled, at least one sign-in)
- A user-mapping CSV with two columns:
source_emailanddestination_upn
Throttling and Reliability
Gmail’s API charges 250 quota units per user per second. A single messages.get with raw format is 5 units. The engine watches the quota headers, spaces calls accordingly, and backs off on 429s with exponential retry. On the Microsoft side, Graph throttling is handled the same way — we respect the Retry-After header and never hammer a throttled endpoint.
Every migrated message is verified: we compare the MIME content-length and message headers between source and destination, and any mismatch re-queues the message. You do not ship a migration that says “done” with silently missing messages.
Related
- Bulk mail migration with user mapping — worked example with a CSV and a common UPN-mismatch problem
- Mail migration guide: Gmail, Office 365, EWS, IMAP — the four-platform overview