Mail Migration: Service-Account Setup
How to register your own Microsoft Entra ID (Azure AD) app with app-only (Application) permissions so MigrationFox can read and write mailboxes across your Microsoft 365 tenant during a bulk mail migration. Use this if your organization prefers its own app registration over admin consent to the MigrationFox Mail app.
Availability
Mail migration is in private beta. If your account shows “Coming Soon · Private Beta” on the Mail page, email hello@migrationfox.com to request access. You can prepare the app registration on this page ahead of time. Attachment files are not copied by default; see Bulk mail migration for details.
What this page covers
This is the setup guide for bulk Microsoft 365 mail migrations with your own app-only credential. The mail wizard supports Gmail and Microsoft 365 (Exchange Online). On-premises Exchange (EWS) and IMAP are not offered. Gmail uses a Google service account instead; see Bulk mail migration. By the end of this page you will have:
- An app registration with two Application permissions, granted with admin consent.
- A client secret for that app, saved in MigrationFox as a Microsoft 365 Mailboxes connection.
- A mapping list that pairs each source mailbox with its destination mailbox.
Which connection type to use
On the project's Connect tab, Microsoft 365 Mailboxes offers three ways to connect. The right one depends on how many mailboxes you are moving and who can grant consent.
| Scenario | Connection type | Why |
|---|---|---|
| Migrating your own mailbox only | Just my own mailbox (sign in) | You sign in and consent; the token covers your mailbox and mailboxes shared with you. No admin involvement required. |
| Migrating 2+ mailboxes, or any CSV / tenant-picker migration | Multiple users in my tenant (admin consent) or Bring your own Azure app | A delegated sign-in can't open other users' mailboxes: a request for user B's mail with a token issued to user A returns 403. |
| Tenant-to-tenant mailbox moves | App-only on both source and destination | There is no practical way to collect a sign-in from every user. |
| Shared mailboxes and former-employee mailboxes | App-only | Nobody signs in as that mailbox, so a tenant-wide app connection is the workable option. |
Rule of thumb: if your list has more than one line, or you don't want each mailbox owner to sign in, use an app-only connection. Multiple users in my tenant is the quicker route (a Global Administrator approves the MigrationFox Mail app once, nothing to copy). The rest of this page covers Bring your own Azure app.
Part 1: Register the app
This registration is the identity MigrationFox uses to call Microsoft Graph in your tenant. A Global Administrator, Application Administrator, or Cloud Application Administrator can create it; granting admin consent for the permissions in Part 2 needs a Global Administrator (or Privileged Role Administrator).
- Sign in to the Microsoft Entra admin center.
- Navigate to Applications → App registrations.
- Click + New registration.
- Name: for example
MigrationFox Mail Migration(any name is fine; it is only for your admins). - Supported account types: Accounts in this organizational directory only (single tenant) is enough when the app is only used in your own tenant.
- Redirect URI: leave blank. App-only credentials never go through a browser redirect.
- Click Register.
- On the app's Overview page, copy 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. Delegated permissions scope a token to a signed-in user; Application permissions scope it to the whole tenant. MigrationFox needs two, the same two the MigrationFox Mail app uses.
| Permission | Type | What it enables |
|---|---|---|
Mail.ReadWrite |
Application | Read folders and messages from source mailboxes; create folders and write messages into destination mailboxes. |
User.Read.All |
Application | List users in the tenant for the Pick users from my tenant picker and for Auto-match. |
To grant them:
- From the app registration, open API permissions.
- Click + Add a permission → Microsoft Graph → Application permissions. (Choose Application, not Delegated. This is the most common setup mistake.)
- Search for
Mail.ReadWrite, check it, click Add permissions. - Repeat for
User.Read.All. - Back on the API permissions list, click Grant admin consent for <your tenant> and confirm.
- Check that each row's Status column reads Granted for <tenant>.
Admin consent is required
Application permissions only take effect after a Global Administrator (or Privileged Role Administrator) clicks Grant admin consent. Adding a permission without consent produces the same 403 errors as if it had never been added. If you don't have the role, add the permissions and ask an admin to grant consent.
Part 3: Create a client secret
The client secret is the password MigrationFox uses to authenticate as the app. Pick an expiry that covers your migration window, and set a reminder to rotate it.
- From the app registration, open Certificates & secrets.
- Under Client secrets, click + New client secret.
- Description: for example
MigrationFox bulk mail. - Expires: choose a period that covers the project (up to 24 months).
- Click Add.
- Copy the Value column immediately and store it somewhere secure. Entra shows the value only once; after you leave the page it is masked. If you miss it, delete the secret and create a new one.
The Secret ID column is not the secret. You want the Value.
Part 4: Add the connection in MigrationFox
You now have three values from Entra:
- Directory (Tenant) ID: from the app's Overview page.
- Application (Client) ID: from the app's Overview page.
- Client Secret Value: the Value you copied in Part 3.
Enter them into MigrationFox:
- Sign in to app.migrationfox.com and open your project.
- Open the Connect tab.
- Click Microsoft 365 Mailboxes, then Bring your own Azure app.
- Paste the Application (Client) ID, Client Secret Value, and Directory (Tenant) ID.
- Save the connection.
MigrationFox checks the connection when you save it. If the check fails, something in Parts 1–3 didn't land; see Troubleshooting below for the common causes.
How the secret is used
The client secret is encrypted (AES-256) before it is stored. When a migration runs, MigrationFox uses it to request short-lived app-only access tokens from Microsoft with the client_credentials grant. App-only flows don't use refresh tokens.
Part 5: Build the mapping list
Bulk mail migrations are driven by a list where each line pairs one source mailbox with one destination mailbox. There are two columns:
| Column | Required? | Notes |
|---|---|---|
sourceEmail | Yes | The address (UPN) of the mailbox to read. Must exist in the source tenant. |
destEmail | Yes | The address (UPN) of the mailbox to write into. Must exist, licensed, in the destination tenant before the migration starts. MigrationFox does not create mailboxes. |
Example:
sourceEmail,destEmail alice@source.contoso.com,alice@target.contoso.com bob@source.contoso.com,bob.smith@target.contoso.com shared-ops@source.contoso.com,operations@target.contoso.com finance@source.contoso.com,finance@target.contoso.com
- Alice: same username in both tenants. The simplest case.
- Bob: the username changed across tenants (
bob→bob.smith). - Shared mailbox: renamed on the destination side. App-only connections open shared mailboxes by address the same way as user mailboxes.
- Finance: a straight one-to-one move.
Folder and date limits are not set per line. The wizard's Options step has a Folder Filter and From Date / To Date that apply to every mailbox in the migration. If different mailboxes need different limits, run them as separate migrations. The full list format (separators, header row, comments) is on the Bulk mail migration page.
Part 6: Start the bulk migration
- In your project, open the Migrate tab and click Start on the Mail card.
- Source: pick Office 365 and the app-only connection for the source tenant.
- Destination: pick Office 365 and the app-only connection for the destination tenant. (For moves within one tenant, source and destination can be the same connection.)
- Mailboxes: choose Paste / CSV mapping, then paste the list or click Upload CSV. Lines that aren't two valid addresses are listed with their line numbers and left out.
- Options: set any folder filter or date range.
- Review: check the mapping preview and click Start migration.
The list runs as one migration that processes the mailbox pairs one after another. A pair that fails (mailbox not found, missing permission) is recorded in the migration's log and does not stop the rest.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| “Office 365 app-only credential requires a target user” | An app-only connection was used with Just my own mailbox. App-only connections have no signed-in user, so every mailbox must be named explicitly. | Choose Paste / CSV mapping or Pick users from my tenant in the Mailboxes step, and make sure each line has both addresses. |
Graph 403 when reading or writing /users/{upn}/messages |
The permission was added without admin consent, or it was added as Delegated instead of Application. | Entra ID → App registrations → your app → API permissions. Confirm each row is Application and its status reads Granted for <tenant>. 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 expired. | Create a new secret in Certificates & secrets, copy the Value column (not the Secret ID), and update the connection. |
| AADSTS700016: Application not found in the directory | The tenant ID points at a different tenant than the one the app is registered in, or the client ID is from a different app. | Copy the Directory (tenant) ID and Application (client) ID again from the app's Overview page. Both must come from the same app registration. |
| The connection works but one specific mailbox returns 403 | An application access policy limits Mail.ReadWrite to a mail-enabled security group, and that mailbox is outside the group. |
Add the mailbox to the policy's group, or widen the policy. See Microsoft's Limit mailbox access guide. |
| A pair fails with a mailbox-not-found error | The destination user doesn't exist, isn't licensed for Exchange Online, or the mailbox hasn't been provisioned yet. | License the user, wait for the mailbox to appear (Get-Mailbox), then run that pair again in a new migration. |
Security and operational notes
This credential grants tenant-wide mailbox access
An app with the Mail.ReadWrite Application permission can read and change every mailbox in the tenant, including executives' mailboxes. Treat the client secret like an administrator password: keep it in a secrets vault, limit who can see it, and rotate it on a schedule. Entra ID sign-in logs (filtered by the Application ID) show when and where the app was used. When the migration project ends, delete the client secret so the app can no longer authenticate.
- Encrypted at rest. MigrationFox encrypts the client secret (AES-256) before storing it.
- Token lifetime. App-only access tokens are short-lived (typically one hour). MigrationFox requests new ones as needed while a migration runs.
- No refresh tokens. The
client_credentialsgrant returns only an access token, so there is no refresh token to manage. - Rotation. When you rotate the secret in Entra, update the connection in MigrationFox before the old secret is removed or expires. A migration that runs with an expired or deleted secret fails to authenticate.
Related
- Bulk mail migration with user mapping: list format, Gmail service account with domain-wide delegation, and the full bulk flow for Gmail and Microsoft 365.
- Power Platform Inventory: a governance scan that also uses an app registration for tenant-wide read access.
- Microsoft: Get access without a user: the underlying
client_credentialsgrant.