🦊 MigrationFox Docs

Mail Migration: Service-Account Setup

How to register an Azure AD application with app-only (Application) permissions so MigrationFox can read and write every mailbox in your Microsoft 365 or Exchange tenant during a bulk mail migration. This is the supported credential type for moving multiple mailboxes — delegated OAuth tokens can only access the signed-in user’s own mailbox.

Availability

Mail Migration is currently gated to SUPER_ADMIN accounts while we finish end-to-end QA against real customer tenants. If you see a “Coming Soon” tile on the dashboard, the feature is built but not yet live on your account. This page is kept current so you can prepare your Azure AD app registration ahead of the general release.

What this page covers

This is the setup guide for bulk mail migrations using an app-only Azure credential against Microsoft 365 (office365) or on-premises Exchange via EWS (exchange-ews). Gmail and IMAP use different auth flows and are linked at the bottom. By the end of this page you will have:

When to use service-account auth vs OAuth delegated

MigrationFox supports both auth styles for Microsoft 365 mail. The right choice depends on how many mailboxes you need to touch and who signs in to consent.

ScenarioCredential typeWhy
Migrating your own mailbox only (single user) OAuth delegated The signed-in user consents, the token is scoped to their mailbox. No admin involvement required.
Migrating 2+ mailboxes, any bulk CSV upload Service account (app-only) Delegated tokens cannot access other users’ mailboxes — /users/B/messages returns 403 when the token was issued to user A.
Tenant-to-tenant mailbox moves (whole organization) Service account (app-only) Required on both source and destination. No practical way to collect delegated consent from every user.
Shared mailboxes, resource mailboxes, former-employee mailboxes Service account (app-only) Nobody is signing in as that mailbox. App-only is the only workable option.
On-prem Exchange (EWS) for multiple mailboxes Service account (app-only) Same rule — one token, many mailboxes. Configure EWS impersonation on the service account.

Rule of thumb: if the CSV has more than one row, or you do not want each mailbox owner to sign an OAuth consent prompt themselves, you need a service-account credential.

Part 1 — Register the Azure AD app

This registration is the identity MigrationFox uses to call Microsoft Graph on behalf of your tenant. Only a Global Administrator, Application Administrator, or Cloud Application Administrator can complete these steps.

  1. Sign in to Microsoft Entra admin center.
  2. Navigate to Applications → App registrations.
  3. Click + New registration.
  4. Name: MigrationFox Mail Migration (any name is fine; this is just for your tenant admins).
  5. Supported account types: Accounts in this organizational directory only (single-tenant). Do not pick multi-tenant — this credential is for your tenant only.
  6. Redirect URI: leave blank. App-only credentials never go through a browser redirect, so no reply URL is needed.
  7. Click Register.
  8. On the app’s Overview page, copy and save the Application (client) ID and the Directory (tenant) ID. You will paste both into MigrationFox later.

Part 2 — Grant Application permissions

App-only credentials only work through Application permissions, not Delegated. The distinction matters: Delegated permissions scope a token to a signed-in user, Application permissions scope a token to the whole tenant. MigrationFox needs three.

PermissionTypeWhat it enables
Mail.ReadWrite Application Read messages and folders from the source mailbox, create folders and write messages to the destination mailbox.
Mail.Send Application Required when an item’s body references drafts that must be re-submitted or when a migration path copies via send-on-behalf semantics.
User.Read.All Application Resolve UPNs and verify target mailboxes exist before the worker opens a long-running job. Keeps the per-pair validation step fast.

To grant them:

  1. From the app registration, open API permissions.
  2. Click + Add a permission → Microsoft Graph → Application permissions. (Take care to click Application, not Delegated — this is the most common setup mistake.)
  3. Search for Mail.ReadWrite, check it, click Add permissions.
  4. Repeat for Mail.Send and User.Read.All.
  5. Back on the API permissions list, click Grant admin consent for <your tenant>. Confirm the prompt.
  6. Verify each row’s Status column reads Granted for <tenant> with a green check.

Admin consent is non-negotiable

Application permissions only take effect after a Global Administrator (or Privileged Role Administrator) clicks Grant admin consent. Adding the permission without granting consent produces the same 403 errors as if the permission was never configured. If you do not have the role yourself, add the permissions and send a screenshot to the admin who does.

Part 3 — Create a client secret

The client secret is the password MigrationFox uses to authenticate as the app. Azure AD supports client secrets up to 24 months; we recommend the 24-month option and a calendar reminder to rotate.

  1. From the app registration, open Certificates & secrets.
  2. Under Client secrets, click + New client secret.
  3. Description: MigrationFox bulk mail (or similar — this is for your own audit trail).
  4. Expires: 24 months.
  5. Click Add.
  6. Copy the Value column immediately and save it somewhere secure. Azure only shows the secret value once — after you navigate away, the column masks to **** and there is no recovery. If you miss it, delete the secret and create a new one.

The Secret ID column is not the secret — you want the Value. A correct secret is a mixed-case string with symbols, typically 40 characters.

Part 4 — Create the MigrationFox credential

You now have three pieces of information from Azure:

Enter them into MigrationFox:

  1. Sign in to app.migrationfox.com.
  2. Navigate to Credentials in the sidebar.
  3. Click + New credential.
  4. Source type: pick Office 365 Mail (for Microsoft 365 tenants) or Exchange (EWS) (for on-prem Exchange).
  5. Auth type: pick Service account (not OAuth).
  6. Paste the Tenant ID, Client ID, and Client Secret into the three fields.
  7. Click Save.

MigrationFox immediately runs a validation call — it requests a token against Microsoft Graph and lists one page of users in your tenant. A green check means the credential is live. A red error means something in Parts 1–3 did not land; see the Troubleshooting table below for the common shapes.

How tokens are stored

The client secret is AES-256 encrypted at rest with a per-install key (CREDENTIAL_ENCRYPTION_SECRET) before it is written to the database. The worker mints short-lived (one-hour) app-only access tokens on demand using the client_credentials grant and never caches them beyond that window. Refresh tokens do not apply to app-only flows.

Part 5 — Build the CSV mapping

Bulk mail migrations are driven by a simple CSV where each row is one source mailbox paired with one destination mailbox. Columns:

ColumnRequired?Notes
sourceEmailYesThe UPN of the source mailbox to read from. Must exist in the source tenant.
destEmailYesThe UPN of the destination mailbox to write to. Must exist in the destination tenant before migration starts — MigrationFox does not provision mailboxes.
startDateOptionalISO-8601 date (YYYY-MM-DD). Only messages received on or after this date are copied.
endDateOptionalISO-8601 date. Only messages received on or before this date are copied.
folderFilterOptionalComma-separated folder names to include. Leave blank to migrate every folder except recoverable-items.

Example CSV:

sourceEmail,destEmail,startDate,endDate,folderFilter
alice@source.contoso.com,alice@target.contoso.com,,,
bob@source.contoso.com,bob.smith@target.contoso.com,2024-01-01,,"Inbox,Sent Items"
shared-ops@source.contoso.com,operations@target.contoso.com,,,
finance@source.contoso.com,finance@target.contoso.com,2023-01-01,2025-12-31,

Row-by-row:

Part 6 — Start the bulk migration

  1. In the MigrationFox dashboard, open Mail Migration.
  2. Click + New job.
  3. Source: pick your service-account credential for the source tenant.
  4. Destination: pick the service-account credential for the destination tenant. (For same-tenant mailbox reshuffles, source and destination are the same credential.)
  5. Click Upload CSV and select your mapping file.
  6. The pair-preview table renders with one row per mailbox pair. Any rows with validation errors (missing UPN, target mailbox not found, bad date format) are flagged in red before you can proceed.
  7. Fix any flagged rows in the CSV and re-upload, or click Skip invalid rows to proceed with the rest.
  8. Click Start. One MigrationJob is queued per valid pair and the cohort is attached to a single Project so you can monitor progress from /projects.

Each job runs through Scan → Transfer like any other migration, with mailbox-specific progress (messages copied, attachments transferred, bytes moved). Failures on individual pairs do not block the rest of the cohort.

Troubleshooting

ErrorCauseFix
“Credential is missing tokens” when saving a service-account credential Legacy behavior that treated every credential as OAuth and expected a refresh token. Fixed April 20, 2026. Anyone still seeing this is on a stale worker — confirm the latest build is deployed. Re-save the credential after the deploy completes.
“Target UPN is required for app-only mail migration” when queuing a job A CSV row omitted destEmail, or the userMapping payload lacks a destination. App-only tokens cannot resolve /me — there is no signed-in user — so every pair must name a target mailbox explicitly. Add destEmail for the flagged row and re-upload. The userPath is always /users/{upn} for app-only credentials.
“Mail.ReadWrite Application permission not granted” (Graph 403 on /users/{upn}/messages) The permission was added to the app registration but admin consent was not granted, or it was added as Delegated instead of Application. Entra ID → App registrations → your app → API permissions. Confirm each row is labeled Application and the Status column reads Granted for <tenant>. Click Grant admin consent again if not.
AADSTS7000215: Invalid client secret provided The value pasted into MigrationFox is the Secret ID (a GUID) rather than the Secret Value, or the secret has since expired. Create a new secret in Certificates & secrets, copy the Value column (not the Secret ID), update the credential.
AADSTS700016: Application not found in the directory The Tenant ID field points at a different tenant than the one the app was registered in, or the Client ID is from a different app. Re-copy Directory (tenant) ID and Application (client) ID from the app’s Overview page. Both must come from the same app registration.
Validation passes but the first job fails instantly with 403 on one specific mailbox Some Microsoft 365 tenants use Application Access Policies to scope Mail.ReadWrite to a mail-enabled security group. A mailbox outside the group still returns 403. Either add the mailbox to the access-policy group, or widen the policy. See Microsoft’s Limit mailbox access guide.

Security and operational notes

This credential grants tenant-wide mailbox access

An app with Mail.ReadWrite Application permission can read and modify every mailbox in the tenant — including the CEO’s. Treat the client secret as you would a Global Administrator password: store it in a secrets vault, limit who can pull it, and rotate on a schedule (annually at minimum; 6 months is better). Audit the app registration regularly — Entra ID → Sign-in logs filtered by Application ID shows exactly when and where the credential was used. If a migration project ends, delete the client secret so the app cannot authenticate even if the remaining record is forgotten.

Related