Copy-GraphMailboxItems.ps1 copies a mailbox folder subtree from one Exchange Online mailbox to another by using Microsoft Graph application authentication with a certificate from Cert:\CurrentUser\My.
The script now supports both same-tenant and cross-tenant copies. You can keep using the legacy single-tenant auth settings, or provide separate source and target tenant/app/certificate settings for tenant-to-tenant copies.
The script can also load default values from a local .env file. Command-line parameters always win over .env values. For same-tenant runs, the legacy TenantId, ClientId, and CertificateThumbprint settings are still enough. For cross-tenant runs, use the source and target auth settings instead.
See SETUP.md for the tenant, application, certificate, and permission prerequisites needed before you run the script.
- Authenticates as an application with a certificate thumbprint.
- Supports separate source-side and target-side Graph authentication contexts.
- Resolves each user's Exchange mailbox ID through Graph.
- Walks the source folder tree recursively.
- Creates matching folders in the target mailbox only when needed for copied content by default.
- Runs a preflight check for ambiguous duplicate target folders before copying.
- Exports mailbox items in full fidelity in batches of up to 20.
- Imports each exported item into the target mailbox.
- Shows a confirmation summary, including estimated folder and item counts and where key values came from, before proceeding unless
-Forceis specified. - Supports a preflight-only mode that validates the copy plan without importing anything.
- Supports an overlay mode that merges the source mailbox root into the target mailbox structure without creating a container folder.
- Can load credentials, mailbox paths, and default switches from a
.envfile. - Supports
-WhatIffor a dry run. - Can include only specific source folders or exclude specific source folders.
- Can copy only items created within an optional date range.
- Always skips
Journal,Conversation History, andRSS Subscriptions. - Treats Calendar, Tasks, Notes, and Contacts copies specially so they target the matching root in the destination mailbox.
This script uses the Microsoft Graph mailbox import/export preview APIs instead of the regular /users/{id}/messages endpoints. That matters because these APIs preserve mailbox items in full fidelity and support restoring them into a different mailbox.
At minimum, the app registration should have:
MailboxFolder.ReadWrite.AllMailboxItem.ImportExport.AllUser.Read.All
If your tenant uses Exchange application RBAC or application access policies, the app also needs to be allowed to the specific source and target mailboxes you want to touch.
For cross-tenant copies, make sure the source-side app has the required permissions and consent in the source tenant, and the target-side app has the required permissions and consent in the target tenant.
The repo includes:
.env.exampleas a template you can copy or compare against.envfor local defaults on this machine
Supported .env keys include:
SOURCE_TENANT_IDSOURCE_CLIENT_IDSOURCE_CERTIFICATE_THUMBPRINTTARGET_TENANT_IDTARGET_CLIENT_IDTARGET_CERTIFICATE_THUMBPRINTTENANT_IDCLIENT_IDCERTIFICATE_THUMBPRINTSOURCE_USER_PRINCIPAL_NAMETARGET_USER_PRINCIPAL_NAMESOURCE_FOLDER_PATHTARGET_FOLDER_PATHIMPORT_DIRECTLY_INTO_TARGET_FOLDEROVERLAY_MODECOPY_EMPTY_FOLDERSINCLUDE_FOLDER_PATHEXCLUDE_FOLDER_PATHOLDESTNEWESTPREFLIGHT_ONLYFORCEEXPORT_BATCH_SIZE
Boolean values accept true/false, yes/no, 1/0, and similar forms. Multi-value include or exclude paths should be separated with ;. If SOURCE_FOLDER_PATH is omitted, the script defaults the source root to \.
Auth fallback behavior:
- If
SOURCE_TENANT_ID,SOURCE_CLIENT_ID, orSOURCE_CERTIFICATE_THUMBPRINTare omitted, the script falls back toTENANT_ID,CLIENT_ID, andCERTIFICATE_THUMBPRINTfor source-side Graph calls. - If
TARGET_TENANT_ID,TARGET_CLIENT_ID, orTARGET_CERTIFICATE_THUMBPRINTare omitted, the script falls back toTENANT_ID,CLIENT_ID, andCERTIFICATE_THUMBPRINTfor target-side Graph calls. - That means existing same-tenant configurations continue to work unchanged.
-SourceUserPrincipalName <string>: Required unless provided in.env. Source mailbox user principal name.-TargetUserPrincipalName <string>: Required unless provided in.env. Target mailbox user principal name.-SourceFolderPath <string>: Optional. Source mailbox folder path to copy. Defaults to\.-TargetFolderPath <string>: Optional. Target mailbox folder path. Defaults to an empty string.-SourceTenantId <string>: Optional. Source Microsoft Entra tenant ID. Falls back to-TenantId.-SourceClientId <string>: Optional. Source app registration client ID. Falls back to-ClientId.-SourceCertificateThumbprint <string>: Optional. Source certificate thumbprint. Falls back to-CertificateThumbprint.-TargetTenantId <string>: Optional. Target Microsoft Entra tenant ID. Falls back to-TenantId.-TargetClientId <string>: Optional. Target app registration client ID. Falls back to-ClientId.-TargetCertificateThumbprint <string>: Optional. Target certificate thumbprint. Falls back to-CertificateThumbprint.-TenantId <string>: Required unless provided in.env. Microsoft Entra tenant ID.-ClientId <string>: Required unless provided in.env. App registration client ID.-CertificateThumbprint <string>: Required unless provided in.env. Certificate thumbprint looked up inCert:\CurrentUser\My.-ImportDirectlyIntoTargetFolder: Optional switch. Import items directly into the selected target folder instead of creating a same-named container folder.-OverlayMode: Optional switch. Merge the source root directly into the target structure.-CopyEmptyFolders: Optional switch. Create empty folders in the destination too.-IncludeFolderPath <string[]>: Optional. Copy only the listed subfolders.-ExcludeFolderPath <string[]>: Optional. Skip the listed subfolders.-Oldest <string>: Optional. Copy only items created on or after this date or timestamp.-Newest <string>: Optional. Copy only items created on or before this date or timestamp.-PreflightOnly: Optional switch. Validate the plan without importing any items.-Force: Optional switch. Skip the confirmation prompt.-EnvFile <string>: Optional. Path to a.envfile to load. Defaults to.env. Pass''to disable.envloading.-ExportBatchSize <int>: Optional. Number of item IDs to export per request. Defaults to20and must be between1and20.
Use a different file at runtime if needed:
.\Copy-GraphMailboxItems.ps1 `
-EnvFile '.env.migration-a' `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-PreflightOnlyTo run without any .env file at all, pass -EnvFile '' and provide the required auth settings on the command line.
Example command-line-only cross-tenant run with .env disabled:
.\Copy-GraphMailboxItems.ps1 `
-EnvFile '' `
-SourceUserPrincipalName 'alex@sourcecontoso.com' `
-TargetUserPrincipalName 'alex@targetfabrikam.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox\Migration' `
-SourceTenantId '11111111-1111-1111-1111-111111111111' `
-SourceClientId '22222222-2222-2222-2222-222222222222' `
-SourceCertificateThumbprint 'SOURCECERTTHUMBPRINT' `
-TargetTenantId '33333333-3333-3333-3333-333333333333' `
-TargetClientId '44444444-4444-4444-4444-444444444444' `
-TargetCertificateThumbprint 'TARGETCERTTHUMBPRINT' `
-ForceCopy from one tenant to another by providing separate source and target auth settings:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'alex@sourcecontoso.com' `
-TargetUserPrincipalName 'alex@targetfabrikam.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox\Migration' `
-SourceTenantId '11111111-1111-1111-1111-111111111111' `
-SourceClientId '22222222-2222-2222-2222-222222222222' `
-SourceCertificateThumbprint 'SOURCECERTTHUMBPRINT' `
-TargetTenantId '33333333-3333-3333-3333-333333333333' `
-TargetClientId '44444444-4444-4444-4444-444444444444' `
-TargetCertificateThumbprint 'TARGETCERTTHUMBPRINT' `
-PreflightOnlyPreview the copy first:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox' `
-WhatIfRun only the preflight validation without copying any items:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox' `
-PreflightOnlyThen run it for real:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox'To skip the confirmation prompt for an unattended run:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox' `
-ForceTo merge a whole mailbox root directly into the target mailbox structure without creating a source-root container folder:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath '\' `
-OverlayMode `
-WhatIfTo import the source folder's contents directly into the selected target folder instead of creating a same-named container folder:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox\Archive' `
-ImportDirectlyIntoTargetFolderTo preserve empty folders too:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox\Projects\FY26' `
-TargetFolderPath 'Inbox' `
-CopyEmptyFoldersTo copy only specific subfolders:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-TargetFolderPath 'Migrated' `
-IncludeFolderPath 'Projects\FY26','Projects\FY27'To copy everything except selected subfolders:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-TargetFolderPath 'Migrated' `
-ExcludeFolderPath 'Newsletters','LowPriority'To copy only items created on or after January 1, 2025:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-TargetFolderPath 'Migrated' `
-Oldest '2025-01-01'To copy only items created on or before January 31, 2025:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-TargetFolderPath 'Migrated' `
-Newest '2025-01-31'To copy only items created within a date range:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Inbox' `
-TargetFolderPath 'Migrated' `
-Oldest '2025-01-01' `
-Newest '2025-01-31'To merge the source Calendar directly into the target mailbox's main Calendar:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Calendar' `
-ImportDirectlyIntoTargetFolderTo copy the source Calendar as a named sub-calendar under the target mailbox's main Calendar:
.\Copy-GraphMailboxItems.ps1 `
-SourceUserPrincipalName 'source@contoso.com' `
-TargetUserPrincipalName 'target@contoso.com' `
-SourceFolderPath 'Calendar' `
-TargetFolderPath 'Migrated Calendar'The same special handling also applies to Tasks, Notes, and Contacts:
- With
-ImportDirectlyIntoTargetFolder, items are merged into the target mailbox's mainTasks,Notes, orContactsfolder. - Without
-ImportDirectlyIntoTargetFolder,TargetFolderPathis treated as the name of a single subfolder to create under the target mailbox's mainTasks,Notes, orContactsfolder.
SourceFolderPathandTargetFolderPathare matched by folder display name.- Command-line parameters override
.envvalues when both are present. - The standard PowerShell
-Verboseswitch is supported when you want additional execution detail. - The script shows a confirmation summary before proceeding unless
-Forceis used. IncludeFolderPathandExcludeFolderPathare mutually exclusive.- Included or excluded folder paths can be relative to
SourceFolderPathor full mailbox-style paths. - Empty folders are skipped by default unless
-CopyEmptyFoldersis used. - A preflight check stops the run early if the target mailbox has duplicate sibling folders with the same name in a location the copy would need to use.
PreflightOnlyruns mailbox resolution, folder discovery, filter planning, and target ambiguity checks, then exits before any copy occurs.OverlayModecurrently supports onlySourceFolderPath '\'and merges the source mailbox root into the target mailbox root or into the folder specified byTargetFolderPath.OverlayModecannot be combined withImportDirectlyIntoTargetFolder.Journal,Conversation History, andRSS Subscriptionsare always excluded from copy, even if explicitly included.Journalexclusion is matched by folder class when available, so it is more resilient across mailbox languages.Conversation HistoryandRSS Subscriptionsexclusions rely on folder name matching in this Graph mailbox API, so non-English mailboxes might still require an explicit-ExcludeFolderPathif Microsoft localizes those folder names differently.- If
SourceFolderPathpoints to a Calendar, Tasks, Notes, or Contacts folder,ImportDirectlyIntoTargetFoldermeans import directly into the target mailbox's matching main folder. - If
SourceFolderPathpoints to a Calendar, Tasks, Notes, or Contacts folder andImportDirectlyIntoTargetFolderis not used,TargetFolderPathis treated as the name of a subfolder to create under the target mailbox's matching main folder. - Calendar, Tasks, Notes, and Contacts special copies only support a single target subfolder name in
TargetFolderPath, not a nested folder path. OldestandNewestfilter on the mailbox item'screatedDateTime.- A date-only
Oldestvalue is treated as inclusive from the start of that date. - A date-only
Newestvalue is treated as inclusive through the end of that date. OldestandNewestcan also be full timestamps, for example2025-01-01T12:30:00Z.- The Graph mailbox import/export APIs are currently
betapreview APIs, so Microsoft can change them. - Export is limited to 20 items per request, which is why the script batches item IDs.