The PowerShell Runbook We Reach For on Every Email Migration

TL;DR
- Email migrations can take weeks, so keeping a PowerShell runbook for both the source and destination environment saves hours of repeated manual work.
- Connecting to Exchange Online, bulk-importing users with passwords, and bulk-updating UPNs are the scripts you run first on the destination tenant.
- Disabling the Exchange throttling policy and setting ApplicationImpersonation on the admin account are the source-side scripts that unblock a third-party migration tool.
- Bulk scripts cover distribution lists with members, shared mailboxes, mailbox permissions, alias add and remove, and mailbox send/receive size changes.
- Get-mailbox piped to get-mailboxstatistics reports every user's mailbox size so you can scope the migration before it starts.

Email migrations can run for weeks, and the way we claw back time is by keeping a PowerShell runbook for both the source and the destination environment. Below is the set of scripts we reach for most often during a migration. Treat it as a copy-and-adapt reference: swap in your own domains, file paths, and admin accounts. If you have a favorite that is not here, we want to hear it.
Connect to an Exchange Online account
The session every other destination-side script depends on:
$credential = Get-Credential
Import-Module MsOnline
Connect-MsolService -Credential $credential
$exchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid/" -Credential $credential -Authentication "Basic" -AllowRedirection
Import-PSSession $exchangeSession -DisableNameCheckingBulk import users with passwords into 365
Create a CSV with the headers: UserPrincipalName, FirstName, LastName, DisplayName, Password.
Import-Csv -Path 'FilePath.csv' | foreach {New-MsolUser -UserPrincipalName $_.UserPrincipalName -FirstName $_.FirstName -LastName $_.LastName -DisplayName $_.DisplayName -Password $_.Password -ForceChangePassword $False}Bulk update UPNs in 365
Moves users from the .onmicrosoft.com domain to the real domain, skipping the admin account:
$UserCredential = Get-Credential
Connect-MsolService -Credential $UserCredential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $UserCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session -AllowClobber
$testpath = test-path c:\temp; If ($testpath -eq $false) {new-item -type directory c:\temp}; $dataout = @();
Get-MsolUser -All | ? {$_.UserPrincipalName -match "domain.onmicrosoft.com" -and $_.UserPrincipalName -notmatch "admin"} | % {Set-MsolUserPrincipalName -ObjectId $_.objectId -NewUserPrincipalName ($_.UserPrincipalName.Split("@")[0] + "@domain.com"); $dataout += "$($_.UserPrincipalName)" ; $_.UserPrincipalName };$dataout | out-file c:\temp\UPNChangeOutput.txtAdd an alias to all users in 365
$LiveCred = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid/" -Credential $LiveCred -Authentication Basic -AllowRedirection
Import-PSSession $Session
$users = Get-Mailbox
foreach ($a in $users) {$a.emailaddresses.Add("$($a.alias)@domain.com")
$users | %{Set-Mailbox $_.Identity -EmailAddresses $_.EmailAddresses}Bulk upload distribution lists with members
Create two CSVs: one with the distribution group name and primary SMTP address, and one with the group name and the members of that group.
Import-Csv -Path 'FilePath.csv' | foreach {New-Distributiongroup -Name $_.Name -PrimarySmtpAddress $_.Address }
Import-Csv 'FilePath.csv' | foreach {Add-DistributionGroupMember -Identity $_.DL -Member $_.Alias}Bulk upload shared mailboxes
Create a CSV with the headers: DisplayName, Userprincipalname.
Import-Csv -Path 'FilePath.csv' | foreach-object { New-Mailbox -Name $_.DisplayName -primarysmtpaddress $_.Userprincipalname -Shared }Bulk upload mailbox permissions
Create a CSV with the headers: Identity (the person you are giving permissions to, format user@domain.com) and User (the person whose mailbox the Identity user will be able to view).
Import-Csv -Path 'FilePath.csv' | foreach {Add-MailboxPermission -Identity $_.Identity -User $_.User -AccessRights FullAccess -InheritanceType All}View users' mailbox size in 365/Exchange
$UserCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $UserCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session
get-mailbox | get-mailboxstatistics | ft displayname, totalitemsizeDisable the throttling policy on Exchange
New-ThrottlingPolicy MigrationPolicy
Set-ThrottlingPolicy MigrationPolicy -RCAMaxConcurrency $null -RCAPercentTimeInAD $null -RCAPercentTimeInCAS $null -RCAPercentTimeInMailboxRPC $null -EWSMaxConcurrency $null -EWSPercentTimeInAD $null -EWSPercentTimeInCAS $null -EWSPercentTimeInMailboxRPC $null -EWSMaxSubscriptions $null -EWSFastSearchTimeoutInSeconds $null -EWSFindCountLimit $null -CPAMaxConcurrency $null -CPAPercentTimeInCAS $null -CPAPercentTimeInMailboxRPC $null -CPUStartPercent $null
Set-Mailbox "<Admin User>" -ThrottlingPolicy MigrationPolicySet impersonation on Exchange/365
Set-ExecutionPolicy Unrestricted
$LiveCred = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $LiveCred -Authentication Basic -AllowRedirection
Import-PSSession $Session
Enable-OrganizationCustomization
New-ManagementRoleAssignment -Role "ApplicationImpersonation" -User admin@domain.comChange mailbox receive/send size in 365
$credential = Get-Credential
Import-Module MsOnline
Connect-MsolService -Credential $credential
$exchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid/" -Credential $credential -Authentication "Basic" -AllowRedirection
Import-PSSession $exchangeSession -DisableNameChecking
Get-Mailbox | Set-Mailbox -MaxReceiveSize 150MB -MaxSendSize 150MBRemove an alias from users
$Records = Get-mailbox -ResultSize Unlimited| where {$_.emailaddresses -like "smtp:*@domain.com"} | Select-Object DisplayName,@{Name="EmailAddresses";Expression={$_.EmailAddresses |Where-Object {$_ -like "smtp:*domain.com"}}}
foreach ($record in $Records)
{
write-host "Removing Alias" $record.EmailAddresses "for" $record.DisplayName
Set-Mailbox $record.DisplayName -EmailAddresses @{Remove=$record.EmailAddresses}
}Remove groups with a certain domain from 365
Get-MsolGroup -all | ?{$_.emailaddress -match "domain.com"} | Remove-MsolGroup -forceFrequently asked questions
Why disable the throttling policy before a migration?
Exchange throttling policies cap concurrent connections, which slows or stalls a migration tool pulling many mailboxes at once. Creating a MigrationPolicy with the limits set to null and assigning it to the admin account removes that ceiling for the duration of the move.
What does ApplicationImpersonation do for a migration?
It lets a single admin account access all user mailboxes without collecting credentials for each one, which is how migration tools read source mailboxes. New-ManagementRoleAssignment -Role ApplicationImpersonation grants it.
How do I check mailbox sizes before migrating?
Connect to Exchange Online and run get-mailbox | get-mailboxstatistics | ft displayname, totalitemsize. This gives you a per-user size list so you can scope bandwidth and timeline before kicking anything off.
From migration scripts to a standing security baseline
PowerShell runbooks move the data. Keeping the destination tenant secure afterward is the recurring job. CloudCapsule checks 250+ controls across every tenant you manage in about 60 seconds each, no scripts to maintain.
Run a free scan
Written by
Nick Ross
CEO · Microsoft MVP · Founder, T-Minus 365
Nick is not just a CEO, he's a respected thought leader and influencer in the MSP space. Tens of thousands of MSPs learn through his YouTube channel, T-Minus365. Nick has been honored as a three-time Microsoft MVP for his educational content; his expertise and influence are the backbone of our mission, ensuring that you are in the best hands when it comes to security.
Nick joined Pax8 in 2017, where he would ultimately oversee product management for PSA and Microsoft integrations. Following his tenure at Pax8, Nick has continued to demonstrate his leadership prowess as an executive at various MSPs, culminating in his most recent role at Sourcepass.
Nick holds a Bachelor's Degree in Business Management from Florida State University, as well as a Minor Degree in Entrepreneurship. In his free time, Nick is an avid hiker, reader, and fitness-junkie.


