Skip to the main content.

ScriptRunner Blog

Migrating Mailboxes to Exchange Online with PowerShell – Part 2

Table of Contents

Post Featured Image

In the first part of this series, Migrating Mailboxes to Exchange Online with PowerShell – Part 1, we covered how to prepare for a mailbox migration, which included a series of preflight checks for mailbox moves. Now we move on to the PowerShell code for moving mailboxes from Exchange Server to Exchange Online. We will walk through the various Move Request cmdlets and how we can use these to perform our main migration tasks. On to the code!


In this section, we will quickly cover the various operations that can be performed with a mailbox when managing their migration to Exchange Online. These actions will be put in the form of PowerShell cmdlets we will need to run.

  • New-MoveRequest: Creates the initial Move Request for moving a mailbox from Exchange Server to Exchange Online.
  • Suspend-MoveRequest: Suspends a migration that is active and places the migration in a suspended or paused state.
  • Set-MoveRequest: Changes some aspect of a Move Request that can be configured.
  • Remove-MoveRequest: Deletes the specified move request and any mailbox data moved will be purged.
  • Get-MoveRequest: Lists the specified move request.

Creating a New Move Request

When moving a mailbox to Exchange Online, there are a few things that you need in order to create the Move Request.

First, we need the email address of the mailbox, which we will call the identity.

Next, we need the Migration Endpoint which is an HTTPS connection point opened up to your local Exchange Servers for Exchange Online to initiate the migration and pull data.

TargetDomain is necessary as this is your tenant domain, usually in this format <tenant>

Credentials, in this case your Active Directory Credentials, are required in order for Exchange Online to create the connection/migration connection.

When creating a Move Request, we also have the option to let the move run to completion or have it suspended when a full sync has completed. The advantage of the latter option is that we can pre-stage mailbox moves for a particular mailbox move event.

After the initial sync, a stage mailbox move will sync once a day to make sure that the latest changes are present in the mailbox copy in Exchange Online. When we are ready to complete the move, the only data to sync are the changes since the last sync, which are usually small, and the completion process is quicker. We can use the 'SuspendWhenReadyToComplete' switch for this in the New-MoveRequest cmdlet.

Let's build a sample Move Request:

$Mailbox = "< Mailbox PrimarySMTPAddress>"
$EndPoint = ""
$TargetDomain = "<tenant>"
$OPCred = Get-Credentials

Putting all of these variables together, we get a New-MoveRequest:

New-MoveRequest -Identity $Mailbox -Remote -RemoteHostName $Endpoint -TargetDeliveryDomain $TargetDomain -RemoteCredential $OPCred -SuspendWhenReadyToComplete -ErrorAction STOP

Using variables also makes the process scalable, where batches of mailboxes can be grouped in a CSV file as a list of Primary SMTP Addresses. Looping through the CSV with a Foreach loop allows us to run as many of these one-liners as we need to move mailboxes in the CSV file.

Completing A Move Request

When staging Move Requests, and when a sync (daily or initial) is completed, the Move Request for the mailbox is put in a suspended state. If we wish to complete the move and have the mailboxes completely transferred to Exchange Online, we need to Resume the Move Request. Using the Resume-MoveRequest cmdlet is the way we can do this.

The one-liner for the resume request is much simpler as the hard work was already done in the Move Request creation. The Resume one-liner looks like this:

Resume-MoveRequest $Mailbox -Confirm:$False

The only criteria needed is the Primary SMTP Address of the mailbox we wish to complete.

Suspending A Move Request

Now, not all mailbox moves go smoothly. Sometimes the Move Requests take up way too much bandwidth and may actually impact a corporation's WAN or Internet link to the point where other projects or business interests are impacted. Well, if our mailbox moves are in full swing, how do we reduce the impact? We have a cmdlet called Suspend-MoveRequest.

The one-liner for the Suspend request is much simpler, as the hard work was already done in the Move Request creation. The Suspend one-liner looks like this:

Suspend-MoveRequest $SMTPAddress -Confirm:$False

The only criteria needed is the Primary SMTP Address of the mailbox we wish to complete.

Deleting A Move Request

On occasion, mailbox Move Requests may need to be removed. This could be because they were created by accident, or a user does not wish to be moved due to other dependencies, or maybe we need to backout of a migration batch and any related requests need to be removed. As such, Microsoft does have a Remove-MoveRequest cmdlet to handle these scenarios. When we run the cmdlet, we are removing the Move Request and on the backend, Microsoft removes all the synced data to be purged at a later date.

The one-liner for the ReMove Request is much simpler, as the hard work was already done in the Move Request creation. The Remove one-liner looks like this:

Remove-MoveRequest $SMTPAddress -Confirm:$False

The only criteria needed is the Primary SMTP Address of the mailbox we wish to complete.

Backout Plan

Backout? Don’t we want all of our mailboxes in Exchange Online so that all those wonderful features Microsoft has created are available to our end users? Well, sure we do, but what if a serious issue comes to light, or we don't have the support we need to Office 365 or a new leader in IT (think CIO) decides that on-premises is the way for the future … Well, then we need some sort of backout plan. For most of the tasks, if the Move Request has not completed, we can simply stop or suspend the Move Request and then delete that Move Request. In doing so, we leave the mailbox on-premises and not moved to Exchange Online. However, what if we have moved the mailbox to Exchange Online, and we need to move it back?

What do we need to move it back?

Moving back to Exchange on-premises requires some detail and knowledge about your Exchange Servers. We need the SMTP Address of the mailbox, the mailbox database to put the mailbox in when it is copied back to Exchange, an Endpoint to connect to (same one we used to move mailboxes to Exchange Online), a target domain (the domain of your Primary SMTP address will do), on-premises credentials and that's it:

$Mailbox = "< Mailbox PrimarySMTPAddress>"
$EndPoint = ""
$TargetDomain = "<>"
$Database = "<Database for mailbox>"
$OpCred = Get-Credential

We also need an Outbound switch to make sure that Exchange Online knows that the request is sending the mailbox out of Exchange Online.

New-MoveRequest -identity $Mailbox -OutBound -RemoteTargetDatabase $Database -RemoteHostName $Endpoint -RemoteCredential $OPCred -TargetDeliveryDomain $TargetDomain

There are some caveats to this, as sometimes a hold will prevent the mailbox from moving, and you may need to remove that hold and then move the mailbox. Otherwise, the move process back to Exchange Server is the same as Exchange Online where the job will sync to 95% and wait for a completion to be initiated and until it does, sync every day.

Converting Single Use to Migration Batches

While operating a single mailbox move at a time might prove useful sometimes, it is better to use PowerShell at scale at all times. Otherwise, efficiencies are lost. Let’s take for example, a new batch of users that needs to be moved. We saw how the New-MoveRequest cmdlet could be used to perform a single task. To covert this to batch use, we first need a CSV file, which we can import into a variable for PowerShell. A simple CSV file would look like this:


Taking the variables from the Move Request section, adding to that a Read-Host one-liner and a Foreach loop we now have this code:

$Mailbox = "< Mailbox PrimarySMTPAddress>"
$EndPoint = ""
$TargetDomain = "<tenant>"
$OPCred = Get-Credentials
$CSV = Read-Host -Prompt "Please specify a CSV file for the current wave of users to move to Office 365"
$WaveCSV = Import-CSV $CSV

Foreach ($Line in $WaveCSV) {

$Mailbox = $Line.SMTPAddress

New-MoveRequest -Identity $Mailbox -Remote -RemoteHostName $Endpoint -TargetDeliveryDomain $TargetDomain -RemoteCredential $OPCred -SuspendWhenReadyToComplete

One caveat to the code is that there is no error checking or error collection, which means if the move fails, we may need to copy the error code from the screen to determine the issue. Also, no logs of good and bad attempts are provided. As such, this code is a quick way to perform mass move/sync of mailboxes to Exchange Online. Similarly, this code framework can be used for suspensions, resumes and deletes of mailbox migrations. Simply insert a one-liner in the Foreach loop that matches the correct operation.


When moving the mailboxes to Exchange Online, it is critical that these migrations be monitored to ensure that they move at a good pace, are not too throttled and complete in a timely manner. We can use PowerShell to run reports and create reporting that in a CSV formatted file that could be useable for analysis purposes. For this we need a couple of cmdlets together: Get-MoveRequest and Get-MoveRequestStatistics. Combined we can run this to see how our migrations are progressing:

Get-MoveRequest | Get-MoveRequestStatistics -ErrorAction STOP | ft DisplayName,Status,PercentComplete

With this one-liner, we will get a sense of progress for all of our moves, with the display of the mailbox name, its migration status as well as what percent the move has completed so far. This same one-liner can also be pushed to output statistics to a CSV file:

$CSV = Read-Host -Prompt "Please specify a CSV file for the current wave of users to move to Office 365"
$Row = "DisplayName,Status,PercentComplete" = Out-File $DestinationFile
Foreach ($Line in $CSV) {
$Move = Get-MoveRequestStatistics -Identity $Mailbox | Select DisplayName,Status,PercentComplete
$DisplayName = $Move.DisplayName
$Status = $Move.Status
$PercentComplete = $Move.PercentComplete
$Row = "$DisplayName,$Status,$PercentComplete"
$Row = Out-File $DestinationFile -Append

CSV Caveats

One thing to remember is that when working with any Move Requests via CSV, the CSV file is not a hard limit. Just because a move was created using one CSV files, does not mean that a Move Request cannot be suspended, deleted, resumed or reported by another. In other words, if there are 25 names in a CSV file at creation, we can create a brand-new CSV files with 15 names or maybe 50 names to manage. Each Move Request is totally independent of each other. So make adjustments as needed, because you will.


In this series of articles on migrating mailboxes to Exchange Online, we explored the various prerequisites as well as mailbox move operations that can all be performed via PowerShell.

We looked at possible roadblocks like invalid SMTP domains as well as policies to be aware of when a mailbox moves to Exchange Online. In terms of Move Requests, we looked at creations, suspension, resuming, completing and even reverting back to Exchange Server on-premises.

What should be clear is that a comprehensive script would have a lot of ground to cover, but it is all doable in PowerShell. We can even further customize the coding to include detailed script logging (possibly a future blog article?), customizing the reporting of mailbox moves and more. PowerShell provides a great tool for handling a bulk task and saves us time from creating all these moves in the GUI.

Related posts

About the author: