Chapter-14 – Practical PowerShell Office 365: Exchange Online

14 Migrations


In This Chapter

  • Introduction
  • Basics (Migrations from On-Premises)
  • Checking Migration Status and Reports
  • Public Folder Migrations



Migrations when it comes to Exchange Online typically involves a couple different scenarios:

  • Migration to Exchange Online from an on-premises solution (Exchange, Lotus Notes, etc.)
  • Migration from Exchange Online to an on-premises solution (Exchange, Lotus Notes, etc.)
  • Migration from Exchange Online tenant to Exchange Online tenant

With respect to PowerShell and Exchange Online, we will cover some very specific scenarios and not all of the scenarios above. For example, the last scenario, a tenant to tenant migration, is typically done with third party tools and beyond setting up users, mailboxes, etc, there really isn't anything else special that PowerShell can do for us in that scenario.

** Note ** Tenant to Tenant migrations will be a feature of Office 365 in the future.

With new versions of Exchange, we now have mailbox moves that can be synced (without completing the migration) to the destination Exchange Server or Exchange Online. With the advent of larger mailboxes this is a rather important advancement. Otherwise it could mean that a migration would take too long to perform in a single big bang, meaning that you have to migrate mailboxes in stages and maintain a coexistence environment until the last mailbox has been moved. It was also a major step towards making Office 365 more accessible to migrate to and more flexible for Microsoft on managing servers and databases.

Note that a mailbox is not locked during the data copy to Office 365 and the user can still access email via their various clients. However, there is an option that is still available, called -ForceOffline switch in the New-MoveRequest cmdlet. You shouldn't have to use it under normal conditions, however from time to time a mailbox is fickle and can only move via an Offline move.

Now, most of the move mailbox options are available from within the Exchange Admin Center in one way or another. But in our experience, EAC is probably fine for simple migrations or the incidental move of one mailbox. If you migrate your server environment from one major build to another, it's almost impossible to ignore PowerShell. Those migrations are far more complex and full of caveats, that it almost always requires the use of custom PowerShell cmdlets and scripts.

But, before delving more into the PowerShell of (Online) Mailbox moves we shall explain the fundamentals of Mailbox moves.

Basics (Migrations from On-Premises)

Although Exchange 2016 creates Migration Batches (more on that later) automatically even when moving one single mailbox, the basis of a mailbox migration is the New-MoveRequest cmdlet. The name is telling; you request the system to move a mailbox. Why is that? Well, it could be the source or more importantly the target server is not in good health before or during the move. The Mailbox Replication service (present on each server in 2016) can decide that the move is too impactful or too risky and stalls the move.

Exchange is responsible for a successful mailbox move to be not to impactful on client experience, due to performance loss (a move does cost extra resources) and preventing data loss during a move.

Another benefit is that the moves can be stopped or synced at any time. This is possible since all move requests are stored in arbitration mailboxes (system mailboxes, hidden from normal view) for processing at any time. Or when a move fails for whatever reason, if you can resolve the issue you can restart the (online) move again. Or you could temporarily suspend any moves when you perceive any issues in your Exchange environment and after resolving those issues, continue at your leisure.

This means the mailbox migrations are a lot more robust and flexible for admins than in previous versions of Exchange (2007 and earlier). You can prepare, pre-stage the data and if necessary troubleshoot your migration long before completing the mailbox moves to the target servers. That moment that is traditionally prone to errors and requiring some aftercare normally. With online mailbox moves, these efforts will often be limited to client issues, rather than (also) server issues.

Migration Batch

As mentioned previously when using the web based Exchange Admin Center, even when you move one single mailbox it will create a Migration Batch. Migration Batches are bulk mailbox moves, which makes those bulk moves more easy to handle: Stopping or suspending them will stop/suspend all moves that are a part of the batch.

Before we create an actual Migration Batch, we need to create a connection point that Exchange Online can use to pull the mailbox data from Exchange on-premises. This connection point is called a Migration Endpoint. The Migration Endpoint consists of a web address (accessible over the Internet) and a set of credentials with which to authenticate with this endpoint. Let's review what cmdlets are available for Migration Endpoints:

Get-Command *MigrationEndpoint

As we can see from the list above, we have our typical PowerShell verbs associated with the MigrationEndpoint noun phrase. Now we can review the examples from the New-MigrationEndpoint:

New-MigrationEndpoint -Name MRSEndpoint -ExchangeRemoteMove -RemoteServer Exchange.MyDomain.Com -Credentials (Get-Credential MyDomain.Com\Administrator)

Once we have our Migration Endpoint created, we can now work on creating a Migration Batch for moving a mailbox to Office 365.

Creating a most basic new batch:

New-MigrationBatch -Name BatchMove01 -SourceEndpoint MRSEndpoint -TargetDeliveryDomain -CSVData ([System.IO.File]::ReadAllBytes("C:\scripting\batch01.csv")) -NotificationEmails

The parameter Local indicates a move to an Exchange Online tenant. The Name is the identifier and CSVData is a CSV file with the mailboxes required to move with this Migration Batch. Also in the line are the SourceEndpoint (where Exchange Online connects to move data). The only column required in the CSV is "EmailAddress", where each mailbox to be migrated should have at least their email address listed, one per line. For information on the format of the input CSV file see:

So, that’s the basic command, but let’s take a look at the other often used cmdlets and parameters. To know what’s already present as a Migration Batch, you’d have to list them with Get-MigrationBatch:

With the AutoComplete parameter, you can tell Exchange that the mailboxes in the Migration Batch may be completed immediately when all the data has been moved of a specific mailbox. With the parameter AutoStart the batch will start at creation, if you do not use this you will have to start the batch manually with:

Start-MigrationBatch -Identity "<batchname>"

You can see the status has (eventually) changed to Syncing, after starting the Migration Batch.

In optimal situations, there are no corrupt items in mailboxes, however based on years of experience there can be many unexpected corruptions that may make an object unreadable or unable to be migrated. Sometimes you can repair those objects, but it is not always practical. Luckily you can configure the Migration Batch to accept a number of corrupt items that will be skipped and thus lost, with the BadItemLimit parameter.

Not only can corrupt items stall a mailbox move, also large items that are over the set MaxReceiveSize value in the organization can halt a move. The LargeItemLimit specifies the number of "violations" that are acceptable, but note that those items are not migrated and thus this can also lead to data/items being lost. You could consider increasing the MaxReceiveSize (and MaxSendSize), using Set-TransportConfig, in the organization or probably better these specific values on the mailbox in the source Exchange environment. Mailbox limits, like these, will stay in effect after the migration, because they are stored in the AD and replicated to Office 365 or the target forest normally. The BadItemLimit and LargeItemLimit parameters are available in the Exchange Admin Center as well, but if you require the use of Exchange PowerShell, these parameters are often a good practice to include. Note that the LargeItemLimit parameter is not valid for local moves, those within the same Exchange organization. Both the BadItemLimit and LargeItemLimit can lead to data loss, so use these parameters with care.

Example of the BadItemLimit parameter:

New-MigrationBatch -Name BatchMove01 -SourceEndpoint MRSEndpoint -TargetDeliveryDomain -CSVData ([System.IO.File]::ReadAllBytes("C:\scripting\batch01.csv")) -AutoStart -BadItemLimit 10 -NotificationEmails

When your environment contains Archive mailboxes in Exchange Server, it might be required to move the primary mailbox separate from the Archive mailbox, you can configure that with the PrimaryOnly or ArchiveOnly parameter. For instance, when you require only Archive mailboxes moved:

New-MigrationBatch -Name BatchMove01 -SourceEndpoint MRSEndpoint -TargetDeliveryDomain -CSVData ([System.IO.File]::ReadAllBytes("C:\scripting\batch01.csv")) -AutoStart -BadItemLimit 10 -NotificationEmails -ArchiveOnly

Whenever you don’t use AutoComplete with your Migration Batch, the batch will automatically synchronize each mailbox every 24 hours. If for whatever reason you do not want this to happen (you expect some performance issues during those synchronization moments for instance), you should use parameter AllowIncrementalSyncs with the value $False. Note that this will result in a longer completion duration which lowers the benefits of an online move.

There are guidelines for the CSV files in order to work correctly with Migration Batches. One requirement is a column with header EmailAddress that will contain the (primary) SMTP address of the mailbox you want to move. Other columns are optional, but when configured will overrule the settings made with MigrationBatch cmdlets. Optional column names are: BadItemLimit and/or MailboxType. For MailboxType this will determine whether the primary, archive mailbox or both are moved (with the values PrimaryOnly, ArchiveOnly and PrimaryAndArchive).

A valid CSV would look like this:

EmailAddress, BadItemLimit, MailboxType, 10, PrimaryAndArchive, 20, Primary

In this example, the Manager's primary mailbox will be with a Bad Item Limit of ten while the Admin's primary mailbox will be moved, the Archive mailboxes will remain on it’s current location and twenty bad items are accepted.

If you get CSV files with some extra columns with attributes not used for mailbox migrations, you could use AllowUnknownColumnsInCsv in order to let Exchange ignore those and only focus on the Identity values.

For those who require regular updates, via email, on the status of the Migration Batch, use the NotificationEmails parameter. It’s a multi-valued string and entries should be delimited with commas. Like so:


Did you forgot a parameter or do you need to change a specific value? Know that you can change certain parameter values of existing Migration Batches with Set-MigrationBatch. For instance:

Set-MigrationBatch -Identity Batch01 -BadItemLimit 10 -AllowIncrementalSyncs $False

Not all values can be changed, for a full overview of the available Set-MigrationBatch parameters check out:

For a full overview of the New-MigrationBatch parameters, what they do and when you should use them check out this page:

Other Migration Batch cmdlets are:






  • Too see the maximum number of batches or concurrent migrations.


  • To change migration configuration settings, such as the maximum number of batches. You can change that from 100 to 200 with:

Set-MigrationConfig -MaxNumberOfBatches 200

Move Requests

Migration Batches create the actual move requests, the actual objects that controls each mailbox move. The batch is there to easily manage move requests in bulk. However, knowing how to control separate move requests is paramount in most successful mailbox migrations.

First, a simple Move Request command in Exchange PowerShell would look like:

New-MoveRequest -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred -BadItemLimit 10 -SuspendWhenReadyToComplete

In this case the Identity value can be different, but we tend to use the Active Directory User Principal Name (UPN) or the primary email address (often they are the same). They are both unique values, user friendly and most objects have an attribute with either of them.

The Move Request will be queued immediately and when the source server is ready the move will begin. During this time the user can still work up until the last percentages. On those last moments, the mailbox moved is finalized; its locked so further changes can't be made to it, the last changes are performed and synchronized, and changes are made in both the source and target Active Directory, if different. There are also a couple of different ways to delay the move finalization that are covered later.

Depending on your source and target destination, the user may have to restart Outlook after their mailbox move is finalized, in order for the Outlook profile to be updated to the new target environment. When users are prompted to restart Outlook was changed with Exchange 2013. With 2013 and higher (including Exchange Online), the server name value stored in Outlook is actually a unique identifier based on the Mailbox ID and the AD domain name and no longer a "server" name. This way Outlook does not have to be restarted as that ID will never change and clients should be connecting to normally a single normally load balanced DNS entry that can point to any Exchange 2016 server. For mailboxes being moved from Exchange 2010 or 2013, the Outlook protocol may also be updated from MAPI/RPC to a HTTPS based connection, using Outlook Anywhere. MAPI over HTTP is the new default protocol for connecting to Exchange Online.

Do note, that you cannot manage Move Requests not part of a Migration Batch via the Exchange Admin Center, so you will have to use Exchange PowerShell to manage the mailbox moves done directly with New-MoveRequest.

This principle of Online Mailbox moves makes migrations a lot more flexible and less prone to risks. For instance, to create a Move Request that will be suspended before it completes, add the SuspendWhenReadyToComplete switch:

New-MoveRequest -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred -SuspendWhenReadyToComplete

It will stop the progress at 95%. This value is not an actual representation of the amount of data migrated, it's just Exchange's internal way of telling you it's practically done. By default, the Move Request will also perform an incremental sync every 24 hours.

So, now we have a Move Request that is suspended. Can we do anything useful with it? Well, yes! With Set-MoveRequest we can change some of the parameters of the move, without stopping and restarting the move from the start! Better yet, even if the move has failed we can still change parameters and restart the move. When it's an Online move (read: from Exchange 2010 or higher) it will restart at the point of failure.

A lot of parameters available within New-MoveRequest are the same as with the New-MigrationBatch cmdlet. This is logical as the Migration Batch in its turn creates a move request per mailbox and subsequently passes on the specified parameter to the move request.

To control the amount of acceptable corrupted objects, before a move fails, you use BadItemLimit with an integer indicating how many failed objects are acceptable. If you have a value of 51 or higher (meaning you accept skipping 50 corrupted objects before the mailbox move fails and stops), you have to add the additional AcceptLargeDataLoss switch otherwise the move will fail. This is an additional safety measure to prevent you from accidentally accepting a data loss of more than 50 items.

In addition, the LargeItemLimit should be used to indicate the number of objects that are allowed to be skipped, that are larger than the message receive limits set on the target. The default message size limit is only valid for mailboxes that inherit the default database limits on the target database, otherwise message size limits set on the mailbox will be used. Another possible reason for failure, is when there are items already in the mailbox that exceed the limits, which existed before specific mailbox item size limits were set.

An alternative to the LargeItemLimit parameter is the AllowLargeItems switch, which is specific for move requests and is not available with Migration Batches. When the AllowLargeItems switch has been added to a New-MoveRequest, Exchange will move all items that exceed the size limits set on target databases or mailbox limits. Both AllowLargeItems and LargeItemLimit should not be used together, the LargeItemLimit will cause the move to fail once the threshold specified has been reached even with the AllowLargeItems switch included. So you cannot use both parameters at the same time since this will result in a failed mailbox move when not expected.

If you have Archive mailboxes you can also control the migration of the primary or archive mailbox separate from each other. The same switches and parameters that control these behaviors are also present. For a description see our section within New-MigrationBatch. The switches and parameters are:

  • ArchiveOnly
  • PrimaryOnly

Every Migration Batch will have a name, but move requests can also have a Batch name defined by using the parameter BatchName. This is a way to easily bulk manage multiple similar move requests. For instance, if you create multiple Move Requests with the same Batch name, you can reference that parameter value. For instance, you’ve created multiple move requests like so:

New-MoveRequest -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred -BatchName "Logistics"

New-MoveRequest -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred "Logistics"

Now you can find them via:

Get-MoveRequest -BatchName "Logistics"

Subsequently you can pipe the output to other cmdlets, for instance Get-MoveRequestStatistics:

Get-MoveRequest -BatchName "Logistics" | Get-MoveRequestStatistics

If you’ve created a Migration Batch, the move request batch name attribute does not correspond exactly with the Migration Batch name value. To give distinction between move requests created via a Migration Batch or a manual action, Migration Batches have the batch name prepended with "MigrationService:". So, if you have any need to investigate specific move requests created via a Migration Batch with the name "Batch01", you could use the following syntax:

Get-MoveRequest -BatchName "MigrationService:Batch01"

Obviously, you can pipe this to other commands. In this case:

Get-MoveRequest -BatchName "MigrationService:Batch01" | Get-MoveRequestStatistics

Another way to influence the impact of your mailbox moves after initial pre-staging has completed, is the frequency of incremental synchronizations. The default value is every 24 hours. The format looks like, where dd:hh:mm:ss stands for days (dd), hours (hh), minutes (mm) and seconds (ss):

New-MoveRequest -Identity "<Mailbox ID>" -IncrementalSyncInterval <dd.hh:mm:ss> -Remote -RemoteHostName -TargetDeliveryDomain practicalpowershell. -RemoteCredential -$LiveCred

For instance, you create a move request and set it to sync every 12 hours with the IncrementalSyncInterval parameter:

New-MoveRequest -Identity "<Mailbox ID>" -IncrementalSyncInterval 00.12:00:0 -Remote -RemoteHostName -TargetDeliveryDomain practicalpowershell. -RemoteCredential -$LiveCred

Normally adjusting the interval is probably not required, however it makes sense if you want specific batches to be started and completed after specific moments in time. You can adjust the sync frequency to suit your needs. You can schedule the start time of a move request with the parameter StartAfter and you can also schedule the completion of a move with the parameter CompleteAfter.

The date value format is dependent on the regional settings of the server and specifically the Short Date value. In this example the format is M/d/yyyy, which translates as 5/23/2016.

New-MoveRequest -Identity <mailboxid> -StartAfter 5/23/2016 -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred

You can also add a specific time, which uses the short time notation in your regional settings. You have to add quotes to the value when you do so:

New-MoveRequest -Identity <mailboxid> -CompleteAfter "6/19/2016 9:59" -Remote -RemoteHostName -TargetDeliveryDomain practicalpowershell. -RemoteCredential -$LiveCred

They can also be combined into one cmdlet:

New-MoveRequest -Identity <mailboxid> -StartAfter 6/10/2016 -CompleteAfter "6/19/2016 9:59" –IncrementalSyncInterval 00:01:00:00 -Remote -RemoteHostName -TargetDeliveryDomain -RemoteCredential -$LiveCred

This will start the Move Request on the 10th of June 2016 and will complete it nine days later at 9:59 (AM in this case, a 24 hour notation was configured in regional settings). It will sync every one hour after the initial synchronization has finished.

The use of the StartAfter and CompleteAfter parameters is recommended by Microsoft. However, there is no specific technical reason to use these versus just manually completing the mailbox moves. Usage of either options to complete moves is dependent on your specific requirements. I personally like to manually perform the completion of moves, mostly because some organizations have a GO/NOGO moment just before completion. You can find the date and time short formats in Control Panel>Region:

Two other switches, PreventCompletion and SuspendWhenReadyToComplete, also prevent the completion of the Move Request. Both of these switches suspend the mailbox move, at 95%, like the CompleteAfter switch. But both of these switches require manually finishing/resuming the mailbox move for it to be completed. Therefore, Microsoft recommends using the CompleteAfter switch to make sure the mailbox moves are completed at some point and not forgotten about.

If you want to create move requests, but be certain that they are not immediately queued and potentially started, you can use the Suspend switch.

When a move request has completed (with or without errors), the requests will be saved up to 30 days and then removed from the system. If you require shorter/longer duration, you can specify this with CompletedRequestAgeLimit. The value is an integer and represents the amount of days:

Set-MoveRequest -Identity <Mailbox ID> -CompletedRequestAgeLimit 120 -Remote -RemoteHostName -TargetDeliveryDomain practicalpowershell. -RemoteCredential -$LiveCred

This will retain the completed Move Request for 120 days.

In some specific cases, it is possible you cannot migrate a mailbox with an online move. In such cases, one thing that might help is to explicitly force an offline move. This will cause the mailbox to be locked as soon as the move request has reached the InProgress status. Unfortunately, this means users cannot access the mailbox until the offline mailbox move has completed.

New-MoveRequest -Identity <Mailbox ID> -ForceOffline -Remote -RemoteHostName -TargetDeliveryDomain practicalpowershell. -RemoteCredential -$LiveCred

** Note ** If there are issues with moving the mailbox at this point, perhaps move the mailbox to another mailbox database for Exchange on-premises, prior to moving a mailbox to Exchange Online.

For a full overview of the parameters, what they do and when you should use them check out this page:

Other cmdlets regarding Move Request are:






Checking Migration Status and Reports

During the mailbox move or pre-staging, you undoubtedly want to monitor the progress and catch any problematic moves. Also in this case the EAC provides basic information that might not be enough for large batch moves.

Even if using Exchange Migration Batches, each mailbox is represented by a Move Request. The Migration Batches are a way to easily manage those multiple Move Requests.


To see the status of current Migration Batches, use:


That only tells us information about Migration Batches and not real info on specific mailbox statuses. You would need Get-MoveRequest for this.

In order to get statistics from a list of all Move Requests independent of their state, you can use:

Get-MoveRequest -ResultSize Unlimited | Get-MoveRequestStatistics

However, you can achieve a more granular view by adding the status:

Get-MoveRequest -ResultSize Unlimited -MoveStatus Completed | Get-MoveRequestStatistics

This will show each move that has the InProgress status equal to Completed. The status options are AutoSuspended, Completed, CompletedWithWarning, CompletionInProgress, Failed, InProgress, None, Queued, and Suspended. Most are evident, however AutoSuspended are those requests that have finished the pre-staging and were given the SuspendWhenReadyToComplete parameter in the Move Request.

If you want to limit your selection to a specific Migration Batch, you can add the BatchName:

Get-MoveRequest -ResultSize Unlimited -BatchName "<batchname>" | Get-MoveRequestStatistics

Note that the BatchName value in New-MoveRequest, created directly, are different than the MigrationBatch value created by EAC and the New-MigrationBatch cmdlet, Exchange adds "MigrationService:" to the latter. So, in order to find all Move Requests from a previously created MigrationBatch use:

Get-MoveRequest -ResultSize Unlimited -BatchName "MigrationService" | Get-MoveRequestStatistics

When the job kicks off, an initialization message like so will appear:

If there are any issues, then they should appear using the same cmdlet. Here is an example error:

Obviously, you can get it even more granular if you add the MoveStatus:

Get-MoveRequest -ResultSize Unlimited -BatchName "<batchname>" -MoveStatus Completed | Get-MoveRequestStatistics


If there are any move requests that have failed, you need to know what the cause of the failure is. Most cases involve too many corrupt/bad items or too many large items, where the limits have been reached. Before increasing those limits, you'd probably want to know whether these are important items or not, for instance a calendar item from five years back is probably not crucial and can be skipped. But sometimes it's an important mail with attachments, you might want to try and extract that object via other means if possible (Outlook perhaps). Or you could decide to perform a New-MailboxRepairRequest on the source mailbox.

Luckily you can investigate each move requests' detailed reporting, you can retrieve that report via:

Get-MoveRequestStatistics -Identity "<move request>" -IncludeReport

However, you won't see that report unless specifically made visible: Via piping to Format-List or (cmdlet).


Get-MoveRequestStatistics -Identity "<move request>" -IncludeReport | Fl

(Get-MoveRequestStatistics -Identity "<move request>" -IncludeReport).report

As you can see, they provide the same information. To export that to a text file, you can use the Export-Csv cmdlet:

Get-MoveRequestStatistics -Identity "<move request>" -IncludeReport | Export-CSV -Encoding UTF8 -Path "<filename>"

The encoding ensures any non-ASCII characters are presented correctly (such as in DisplayName values, etc.). The Path is the path and filename of the export file.

If you require bulk export of reports, a Foreach loop is required. The basic principle would be:

$MoveRequests = Get-MoveRequest -ResultSize Unlimited

Foreach ($MoveRequest in $MoveRequests){

(Get-MoveRequestStatistics -Identity $MoveRequest.Identity -IncludeReport).Report | Export-CSV -Encoding UTF8 -Path "$MoveRequest.txt"}

First all move requests are stored in a variable. Then for each request, the Get-MoveRequestStatistics cmdlet is performed on them. That cmdlet retrieves the move statistics report, which is then exported to a TXT file (in CSV format) with the DisplayName of the mailbox as a file name.

Obviously, there are variants possible, depending on your requirements. Most importantly you can select specific move requests by adding more filters. Check the beginning of the chapters on how to leverage Get-MoveRequest in order to get the reports you need. Be sure to also check out the chapter on Reporting with PowerShell.

Public Folder Migrations

Yes, Public Folders just won't die on us… Starting in Exchange 2013, the way Public Folder data is stored has changed dramatically. From a separate type of database (Public Folder Database as opposed to Mailbox Database), data is now stored in Public Folder Mailboxes inside Mailbox Databases. End-user experience hasn't changed, but the administration is somewhat different and more importantly the migration from legacy Public Folders to Modern Public Folders is very different.

Simply put, the move is akin to the Mailbox Move Request; the mailbox replication service is responsible for synchronizing Public Folder data across Public Folder mailboxes. This is initially staged online, which means users can continue to view/edit Public Folders during this stage. After the initial stage, incremental syncs are performed until the moment of switchover and a relatively short downtime of Public Folders.

The process is best described in this TechNet Article:

Use batch migration to migrate legacy public folders to Office 365 and Exchange Online:

Use batch migration to migrate modern public folders to Office 365 and Exchange Online: