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:
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 group in the source tenant called MigUsers@sourcedomain.com and add all of the 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 code bits in, then save it for later. First, we need our commands to be able to switch quickly between tenants:
# 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 above to set up the App Registration and Enterprise App, easy as bro!):
# 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="MigUsers@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:
# 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 from 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:
# get source mailbox details
Get-Mailbox | select DisplayName, UserPrincipalName, PrimarySMTPAddress, ExchangeGUID, ArchiveGUID, LegacyExchangeDN | Export-Csv C:\temp\sourceusers.csv -NoTypeInformation
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'
Once this is working and we are confident all is well, copy then open the CSV output and add columns ‘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.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 into the file and populate the other columns as below:

Now you can use this code to create the MailUser objects with the first CSV file:
# TO DO BATCHES
# UPDATE THESE CORRECTLY - set variables and import csv
$batchname = 'batch1'
$csv = Import-Csv C:\temp\sourcedomain\batch1.csv
foreach ( $line in $csv ) {
$originalalias = $line.OriginalAlias
$newalias = $line.NewAlias
$userdisplayname = $line.DisplayName
$exchguid = $line.ExchangeGuid
$archguid = $line.ArchiveGuid
$x500address = "X500:" + $line.LegacyExchangeDN
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newalias + "@targetdomain.com"
$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
When the mailboxes in a batch have ‘Synced’ status, go ahead and complete them using this command:
# Complete a Synced batch
Complete-MigrationBatch $batchname -Confirm:$false
Done! Once completed the ‘mailuser’ you created in the target tenant will become a ‘mailbox’ 👍
*@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@*@**@
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… a far less perilous journey I hope, but really I’m impressed with how relatively easy this was!
Until next time… ‘Hei kone ra’ from New Zealand Aotearoa! 🍻😜
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?
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
Greetings! Can you clarify, explain to me where and on what item is taken: $scope=”MigUsers@sourcedomain.com”
I missed this moment. Thank you!
Hi rkorniichuk, MigUsers@sourcedomain.com is a group you create in the source tenant. It must contain all of the users you will migrate to the target tenant.
Cheers!
Simon