Microsoft 365 cross-tenant migration

Hello! Long time no… 🍻😊🍻

I thought I would share some PowerShell I used during a recent cross-tenant migration. Firstly, the Microsoft documentation is really good and got the journey off to a good start:

PLEASE NOTE: Following is some fairly raw PowerShell. You can’t just press Go! You’ll need to understand and update the bits required and run accordingly! Hopefully everything that requires updating is in bold-italics. If not please punish me via comment! =)

That said… let’s continue 😃:

Firstly, let’s create a mail-enabled security group in the source tenant Exchange Admin console, ‘’ should do the trick! ..then add the users (mailboxes) you will migrate (the migration endpoint is scoped to a group, anyone not in the group will fail to migrate).

Now load PowerShell ISE and paste all of the below code bits in, then save it for later. First, we need our commands to be able to switch quickly between tenants (disconnect before connecting each time):

# Source tenant
Connect-ExchangeOnline -UserPrincipalName
# Target tenant
Connect-ExchangeOnline -UserPrincipalName
# Disconnect from tenant
Disconnect-ExchangeOnline -Confirm:$false

Now we can connect to the target tenant and create the Org Relationship and Migration Endpoint – you’ll need the ‘sourcedomain‘, app ID and secret to paste in here where the bold italics are (follow the MS article to set up the App Registration and Enterprise App, easy as!): Target tenant app setup.

# connect to target tenant here
# Enable customization if tenant is dehydrated
  $dehy = Get-OrganizationConfig | fl isdehydrated
  if ($dehy -eq $true) {Enable-OrganizationCustomization}

# Create Migration Endpoint in target tenant
$AppId = "paste the app id here"
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppId, (ConvertTo-SecureString -String "paste the app secret here" -AsPlainText -Force)
New-MigrationEndpoint -RemoteServer -RemoteTenant "" -Credentials $Credential -ExchangeRemoteMove:$true -Name "SourceDomainMigEndpoint" -ApplicationId $AppId

# Create Org Relationship in target tenant
$sourceTenantId="paste source tenant id here"
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $sourceTenantId}
If ($null -ne $existingOrgRel)
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound
If ($null -eq $existingOrgRel)
    New-OrganizationRelationship "SourceDomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound -DomainNames $sourceTenantId

Let’s use our disconnect / connect commands above to disconnect from the target tenant and connect to the source tenant to set up the other side:

# connect to source tenant here

# Configure OrgRel in source tenant
$targetTenantId="insert the target tenant ID here"
$appId="insert the app id here"
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $targetTenantId}
If ($null -ne $existingOrgRel)
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope
If ($null -eq $existingOrgRel)
    New-OrganizationRelationship "targetdomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -DomainNames $targetTenantId -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope

Hopefully you got to this point without issue – if not let me know in the comments section and I’ll try to help! Next, we should be getting a successful test of what we have set up. Run this command since you are still connected to the source tenant – you should see the expected values returned:

# Confirm OrgRel in Source tenant
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability

Now, disconnect and connect to the target tenant again, and run this:

# Confirm OrgRel and MigEndpoint in Target tenant
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability
Test-MigrationServerAvailability -Endpoint "SourceDomainMigEndpoint"

Awesome, green lights!? The tenants are configured. Now disconnect / connect to the source tenant, we’ll need to get the following details out into a CSV file so we can create the MailUser objects in the target tenant (yes I know we could have done that earlier =)). Limit this if required based on the group we created earlier, scoped to your migration users.

# get source mailbox details
Get-Mailbox | select DisplayName, UserPrincipalName, PrimarySMTPAddress, ExchangeGUID, ArchiveGUID, LegacyExchangeDN | Export-Csv C:\temp\sourceusers.csv -NoTypeInformation

Disconnect / connect to the target tenant again. We want to test one user first, so I used these commands (replace bold italic with values for one user from the CSV output):

# create MailUsers in target tenant - ONE AT A TIME HERE OR BELOW FOR BATCHES
$originalalias = 'sales'
$newalias = 'sourcedomain.sales'
$userdisplayname = 'sourcedomain Sales Department'
$exchguid = '80ddefd4-26cb-7621-c497-g6c044b04dd9'
$archguid = '70edegc5-36f8-8639-b287-f6g035408ec2'
$x500address = 'x500:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=ce04185c5ff841f885d660e46e6c8bc5-sourcedomain SAL'
$usersourceaddress = $originalalias + ""
$usertargetaddress = $newalias + ""
$usertargettenantaddress = $newalias + ""
New-MailUser -Name $newalias -DisplayName $userdisplayname -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}

Then we can migrate the user:

New-MigrationBatch -Name testbatch -SourceEndpoint 'sourcedomainMigEndpoint' -UserIds $usertargetaddress -Autostart -TargetDeliveryDomain ''

NOTE: Recently I found this command was not working and I had to use a one-liner copy of the batch1-users.csv mentioned below. After the test user is successfully migrated, reference your original file with all the mailboxes in it. Just in case you get this issue in your scenario 😃

Okay! When this is working and we are confident all is well, copy the sourceusers.csv to a new file named batch1-detail.csv. Open the file and add three columns before DisplayName as below. ‘originalalias’ (the bit before the UPN), ‘newalias’ (set as needed for the target tenant) and ‘newupn’ (new UPN in the target tenant):

Assuming the file is called batch1-detail.csv, create another CSV called batch1-users.csv. This is the input for the migration batch command (very simple and worked well for me), just copy the target UPNs from batch1-detail.csv into the EmailAddress column and populate the other columns as below:

Now you can use this code to create the MailUser objects with the batch1-detail.csv file:

# UPDATE THESE CORRECTLY - set variables and import csv
$csv = Import-Csv C:\temp\sourcedomain\batch1-detail.csv

foreach ( $line in $csv ) {

$originalalias = $line.OriginalAlias
$newalias = $line.NewAlias
$newupn = $line.NewUPN
$userdisplayname = $line.DisplayName
$exchguid = $line.ExchangeGuid
$archguid = $line.ArchiveGuid
$x500address = "X500:" + $line.LegacyExchangeDN
$usersourceaddress = $originalalias + ""
$usertargetaddress = $newupn
$usertargettenantaddress = $newalias + ""

Write-Host -ForegroundColor Green "Processing $usertargetaddress..."

New-MailUser -Name $newalias -DisplayName $userdisplayname -PrimarySmtpAddress $usertargetaddress -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}
Get-MailUser $usertargetaddress | fl DisplayName,PrimarySMTPAddress,ExchangeGuid,ArchiveGuid

Now we’ve created the MailUser objects for our batch, we can migrate them using the second file:

# create migrationbatch using csv
$batchfile = 'C:\temp\sourcedomain\batch1-users.csv'

New-MigrationBatch -Name $batchname -SourceEndpoint 'sourcedomainMigEndpoint' -CSVData ([System.IO.File]::ReadAllBytes("$batchfile")) -Autostart -TargetDeliveryDomain ''

NOTE: At this point you are migrating the mailboxes because of the “-Autostart” switch. Users in the batch will have ‘Syncing’ status. When ready to complete they will be ‘Synced’.

You can run this command to check the status of a batch:

# Get status of a migration batch:
Get-MigrationBatch $batchname | fl

Or this to check status of a single user:

# Get details for a single mailbox
Get-MigrationUser 'insertupnhere' | Get-MigrationUserStatistics | fl

Or this to get the status of all migration users, updated every 5 minutes:

# output progress of all users every 5 mins
$x = 0
$x = $x + 1
Get-MigrationUser | Get-MigrationUserStatistics

Start-Sleep -Seconds 300
Until ( $x -eq 480 )

When the mailboxes in a batch have ‘Synced’ status, go ahead and complete the batch using this command:

# Complete a Synced batch
Complete-MigrationBatch $batchname -Confirm:$false

This can take a bit of time so have a beer or cup of tea then come back – done! Once completed the mailuser you created in the target tenant will become a mailbox, and the mailbox in the source tenant will become a mailuser with a forwarding address set to the users target address 👍

Some other commands I used are:

# Restart a failed batch
## Start-MigrationBatch $batchname

# remove a completed batch
## Remove-MigrationBatch $batchname -Confirm:$false

# Remove failed users from batches due to failure
## Get-MigrationUser | ? { $_.status -eq 'failed' } | Remove-MigrationUser -Confirm:$false

This migration went really smoothly… once the user was migrated, the forwarding was set correctly in the source tenant, and permissions were intact for access to Shared Mailboxes etc. Now we are planning to tidy up and move additional email aliases, the accepted domains and MX records across to the new tenant… I’m impressed with how relatively easy this was!

Until next time… ‘Hei kone ra’ from New Zealand Aotearoa! 🍻😜