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:

https://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration?view=o365-worldwide

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, ‘zzMigUsers@sourcedomain.com’ 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 migadmin@sourcedomain.com
# Target tenant
Connect-ExchangeOnline -UserPrincipalName migadmin@targetdomain.com
# 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 outlook.office.com -RemoteTenant "sourcedomain.onmicrosoft.com" -Credentials $Credential -ExchangeRemoteMove:$true -Name "SourceDomainMigEndpoint" -ApplicationId $AppId

# Create Org Relationship in target tenant
$sourceTenantId="paste source tenant id here"
$orgrels=Get-OrganizationRelationship
$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"
$scope="zzMigUsers@sourcedomain.com"
$orgrels=Get-OrganizationRelationship
$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-MigrationEndpoint
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 + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newalias + "@targetdomain.com"
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"
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 'targetdomain.onmicrosoft.com'

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:

# TO DO BATCHES
# 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 + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newupn
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"

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

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 'targetdomain.onmicrosoft.com'

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
Do{
$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 onmicrosoft.com 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! 🍻😜

Loading

12 thoughts on “Microsoft 365 cross-tenant migration”

  1. Hi Simon,

    I am getting error of:

    Migration rate:
    Error: CommunicationErrorTransientException: The call to ‘net.tcp://as8p192mb1222.eurp192.prod.outlook.com:64340/Microsoft.Exchange.MailboxReplicationService.ProxyService’ failed. Error details: The server has rejected the client credentials. –> The server has rejected the client credentials. –> The logon attempt failed. –> The server has rejected the client credentials. –> The server has rejected the client credentials. –> The logon attempt failed

    1. Were you ever able to figure this out? I am getting the same error when trying to do a cross-tenant migration.

      1. Hi Mike, this can also occur if Admin Consent has not been granted in both tenants for the migration App. After step 18 in the ‘Prepare target tenant’ section of Microsoft documentation, there are another 5 steps – please check these were done as below:

        Now that you’ve successfully created the migration application and secret in the target tenant, you’ll need to consent to the application. To consent to the application:
        1. Go back to the Azure Active Directory landing page, select on Enterprise applications in the left navigation, find your migration app you created, select it, and select Permissions on the left navigation.
        2. Select on the “Grant admin consent for [target tenant]” button.
        3. A new browser window will open – select “Accept”.
        4. You can go back to your portal window and select Refresh to confirm your acceptance.
        5. Formulate the URL the source tenant admin must use so they can also consent to the application to enable mailbox migrations. Here’s an example of the URL – you’ll need to replace the contoso.onmicrosoft.com with the source tenant URL, and [application_id_of_the_app_you_just_created] with the application ID of the app you created in the target tenant:
        https://login.microsoftonline.com/contoso.onmicrosoft.com/adminconsent?client_id=%5Bapplication_id_of_the_app_you_just_created%5D&redirect_uri=https://office.com

        Here is an example if my tenant was the source, and the target tenant App ID was fb4e330e-36c8-4657-b6f7-c4ec759e4231:
        https://login.microsoftonline.com/howdoiuseacomputer.onmicrosoft.com/adminconsent?client_id=fb4e330e-36c8-4657-b6f7-c4ec759e4231&redirect_uri=https://office.com

        Until Admin consent is provided for the app in both source and target tenants, migrations will fail.

        Hope that helps!

  2. Conrad Esterhuyse

    Hi Simon
    Thank you for a great tutorial!
    Just a thought…Might be useful to have some examples of the output to expect when testing or confirming what we did. This is my first time attempting a tenant-to-tenant migration, I am not always sure whether my “green lights” are the same as your “green lights”.

    1. Good point Conrad! Thanks for your comment. I wrote the post as an afterthought, so only had the code I’d used, not the results unfortunately… 🙁 saved to the memory banks for next time!

  3. I got this error while doing the migration: ProxyAddressMissingOnMEUPermanentException: One or more X500 proxy addresses or legacyExchangeDN of source mailbox are missing on target MEU as X500 proxy address.

    1. Hi Sai, okay I would check the source and destination X500 addresses in the ProxyAddresses attribute… does the start of the addresses have x500:, or has that been doubled up maybe?
      This is the line that creates the target X500 address which should match the LegacyExchangeDN in the source tenant.
      $x500address = ‘x500:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=ce04185c5ff841f885d660e46e6c8bc5-sourcedomain’

      Cheers, Simon

  4. Really great more simple than microsoft doc, but I have a question/trouble, I create one migration batch with all users (only 15) and dont give any error but the status is always “Syncing” after 3 days I start. I remove the migration batch and start the process again, and I try only 1 batch with one user, but its the same problem. What is the problem?
    I have another question, the recipienttype is “mailuser” but after that I can assign license and transfor to usermailbox or sharedmailbox?

    1. Hi Fernando, firstly, what size are the mailboxes, and are they showing as ‘Syncing’ or ‘Synced’? You can try to get more information about the status using this command:
      Get-MigrationUser ‘insertUPNhere’ | Get-MigrationUserStatistics | fl
      Hopefully this will give you more information to work with. Also thanks for pointing out I had not added how to complete the batches! Oops! 🙂 🙂 I have updated the post.

      Secondly, yes the target object is a ‘mailuser’ when you create it. When you complete a migration batch (using the Complete-MigrationBatch command), it will automatically be converted to a fully functioning ‘mailbox’, regardless of whether a license is assigned or not (without a license you will not see the Outlook icon at office.com, but you can still use Outlook app or with direct browser connection to https://outlook.com/domainname.com). You will need to assign a license within 30 days, or it will be soft-deleted as usual. 30 days after that it will be permanently deleted.

      Cheers,
      Simon

  5. Greetings! Can you clarify, explain to me where and on what item is taken: $scope=”MigUsers@sourcedomain.com”
    I missed this moment. Thank you!

Leave a Reply to simon.burbery Cancel Reply

Your email address will not be published. Required fields are marked *

Scroll to Top