A comprehensive PowerShell solution for migrating email aliases from on-premise Exchange to Exchange Online safely and efficiently.


Download Scripts from GitHub

Overview

When migrating from on-premise Exchange to Exchange Online (Office 365), one of the most challenging aspects is preserving email aliases. Users often have multiple email addresses that need to be maintained for business continuity. This PowerShell solution provides a safe, automated way to export aliases from your on-premise environment and import them to Exchange Online.

✅ Safe Operations

Scripts only add aliases, never remove existing ones. Your current Exchange Online configuration remains intact.

Flexible Targeting

Target specific databases, organizational units, or use custom filters to scope your migration precisely.

Preview Mode

WhatIf support lets you preview all changes before applying them, ensuring confidence in your migration.

Progress Tracking

Real-time progress indicators and comprehensive logging help you monitor large migrations.

Structured Export

JSON format provides easy review, backup, and audit trail of your migration data.

Enterprise Ready

Built for production environments with error handling, retry logic, and detailed reporting.

Prerequisites

For On-Premise Exchange (Export)

  • PowerShell 5.1 or later – Required for script execution
  • Exchange Management Shell – Or remote PowerShell access to Exchange server
  • Administrative Permissions – Organization Management or Recipient Management role
  • Network Access – Connectivity to Exchange server from script execution location

For Exchange Online (Import)

  • PowerShell 5.1 or later – Required for script execution
  • ExchangeOnlineManagement Module – Microsoft’s official PowerShell module
  • Exchange Online Admin Rights – Global Admin or Exchange Administrator role
  • Modern Authentication – Support for OAuth 2.0 and conditional access policies

Installation Guide

1

Install Exchange Online Management Module

This module is required for connecting to Exchange Online and managing mailboxes.

# Install the module (run as Administrator)
Install-Module -Name ExchangeOnlineManagement -Force -AllowClobber# Verify installation
Get-Module -ListAvailable -Name ExchangeOnlineManagement
2

Download the Scripts

Get the latest version of both PowerShell scripts from the GitHub repository.

# Clone the entire repository
git clone https://github.com/MSB365/Mail-Alias-Migration.git
cd Mail-Alias-Migration# Or download individual files directly from GitHub

Direct Download Links:

3

Configure PowerShell Execution Policy

Allow the scripts to run by setting the appropriate execution policy.

# Set execution policy for current user
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser# Verify the setting
Get-ExecutionPolicy -List

⚡ Quick Start Guide

Pro Tip: Always start with a small test group to validate the process before running large migrations.
1

Export from On-Premise Exchange

Run the export script on a machine with Exchange Management Shell access.

# Export all mailboxes from a specific database

.\Export-OnPremiseMailboxes.ps1 -Database "Mailbox Database 01"# Export with department filter

.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Sales'"

# Export from specific OU
.\Export-OnPremiseMailboxes.ps1 -OrganizationalUnit "OU=Users,DC=contoso,DC=com"
2

Review the Export Data

Examine the generated JSON file to verify the export captured the expected data.

# Load and review export summary
$data = Get-Content .\mailbox-addresses.json | ConvertFrom-Json
Write-Host "Exported $($data.Mailboxes.Count) mailboxes"# View detailed information
$data.Mailboxes | Format-Table DisplayName, PrimarySMTPAddress, @{Name="Aliases";Expression={$_.EmailAliases.Count}}
3

Test Import (WhatIf Mode)

Preview what changes will be made without actually applying them.

# Preview changes without applying them
.\Import-MailboxAliases.ps1 -JsonPath .\mailbox-addresses.json -WhatIf
⚠️ Important: Always run in WhatIf mode first to verify the changes look correct before proceeding with the actual import.
4

Perform the Import

Execute the actual import to add aliases to Exchange Online mailboxes.

# Import aliases to Exchange Online
.\Import-MailboxAliases.ps1 -JsonPath .\mailbox-addresses.json

Export Script: Export-OnPremiseMailboxes.ps1

This script connects to your on-premise Exchange server and exports mailbox information including primary SMTP addresses and all aliases to a structured JSON file.

Parameters

Parameter Type Required Description
Server String No Exchange server FQDN (uses auto-discovery if not specified)
Database String No Target specific mailbox database for scoped export
OrganizationalUnit String No Target specific organizational unit (OU) path
Filter String No Custom PowerShell filter expression for mailbox selection
OutputPath String No JSON output file path (default: .\mailbox-addresses.json)
Credential PSCredential No Alternative credentials for Exchange connection

Usage Examples

Database-Specific Export

# Export all mailboxes from a specific database
.\Export-OnPremiseMailboxes.ps1 -Database "Executive Mailbox Database"# Export with custom output location
.\Export-OnPremiseMailboxes.ps1 -Database "Sales DB" -OutputPath "C:\Migration\sales-export.json"

Department-Based Export

# Export Marketing department
.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Marketing'"# Export multiple departments
.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Sales' -or Department -eq 'Marketing'"

# Export with location filter
.\Export-OnPremiseMailboxes.ps1 -Filter “Department -eq ‘IT’ -and Office -eq ‘New York'”

Organizational Unit Export

# Export from specific OU
.\Export-OnPremiseMailboxes.ps1 -OrganizationalUnit "OU=VIP,OU=Users,DC=company,DC=com"# Export executives
.\Export-OnPremiseMailboxes.ps1 -OrganizationalUnit "OU=Executives,DC=contoso,DC=com"

Common Filter Examples

# Department-based filters
-Filter "Department -eq 'IT'"
-Filter "Department -like '*Sales*'"# Location-based filters
-Filter "Office -eq 'London'"
-Filter "Office -ne 'Temporary'"

# Title-based filters
-Filter “Title -like ‘*Manager*'”
-Filter “Title -like ‘*Director*’ -or Title -like ‘*VP*'”

# Exclude test accounts
-Filter “DisplayName -notlike ‘Test*’ -and DisplayName -notlike ‘Demo*'”

# Custom attribute filters
-Filter “CustomAttribute1 -eq ‘VIP'”
-Filter “CustomAttribute2 -ne ‘Exclude'”

# Complex combinations
-Filter “Department -eq ‘Sales’ -and Office -eq ‘NYC’ -and Title -like ‘*Manager*'”

Import Script: Import-MailboxAliases.ps1

This script reads the JSON file created by the export script and safely adds the aliases to corresponding mailboxes in Exchange Online. It only adds aliases and never removes existing ones.

Parameters

Parameter Type Required Description
JsonPath String Yes Path to JSON file created by export script
WhatIf Switch No Preview changes without applying them
Credential PSCredential No Exchange Online credentials (uses interactive auth if not provided)
TenantId String No Azure AD Tenant ID for multi-tenant environments

Usage Examples

Basic Import Operations

# Basic import
.\Import-MailboxAliases.ps1 -JsonPath .\mailbox-addresses.json# Preview mode (recommended first step)
.\Import-MailboxAliases.ps1 -JsonPath .\mailbox-addresses.json -WhatIf

# Import with progress monitoring
.\Import-MailboxAliases.ps1 -JsonPath .\sales-export.json -Verbose

Multi-Tenant and Credential Scenarios

# Specific tenant
.\Import-MailboxAliases.ps1 -JsonPath .\export.json -TenantId "12345678-1234-1234-1234-123456789012"# With stored credentials
$cred = Get-Credential -Message "Enter Exchange Online Admin Credentials"
.\Import-MailboxAliases.ps1 -JsonPath .\export.json -Credential $cred

# Service principal authentication
$appCred = New-Object System.Management.Automation.PSCredential(“app-id”, $secureSecret)
.\Import-MailboxAliases.ps1 -JsonPath .\export.json -Credential $appCred -TenantId “tenant-id”

Advanced Usage Examples

Example 1: Phased Department Migration

Migrate departments one at a time to minimize risk and allow for validation between phases.

# Phase 1: Sales Department
.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Sales'" -OutputPath ".\sales-phase1.json"# Review sales data
$salesData = Get-Content .\sales-phase1.json | ConvertFrom-Json
Write-Host "Sales users to migrate: $($salesData.Mailboxes.Count)"
$salesData.Mailboxes | Select DisplayName, PrimarySMTPAddress | Format-Table

# Test import
.\Import-MailboxAliases.ps1 -JsonPath .\sales-phase1.json -WhatIf

# Execute import
.\Import-MailboxAliases.ps1 -JsonPath .\sales-phase1.json

# Phase 2: Marketing Department
.\Export-OnPremiseMailboxes.ps1 -Filter “Department -eq ‘Marketing'” -OutputPath “.\marketing-phase2.json”
.\Import-MailboxAliases.ps1 -JsonPath .\marketing-phase2.json -WhatIf
.\Import-MailboxAliases.ps1 -JsonPath .\marketing-phase2.json

# Continue with additional departments…

Example 2: VIP User Priority Migration

Prioritize executive and VIP users for early migration to ensure business continuity.

# Export VIP users first
.\Export-OnPremiseMailboxes.ps1 -Filter “Title -like ‘*Director*’ -or Title -like ‘*VP*’ -or Title -like ‘*CEO*’ -or Title -like ‘*President*'” -OutputPath “.\vip-users.json”# Verify VIP list
$vipData = Get-Content .\vip-users.json | ConvertFrom-Json
Write-Host “VIP users identified: $($vipData.Mailboxes.Count)”
$vipData.Mailboxes | Select DisplayName, Title, PrimarySMTPAddress, @{Name=”Aliases”;Expression={$_.EmailAliases.Count}} | Format-Table# Migrate VIP users with extra caution
.\Import-MailboxAliases.ps1 -JsonPath .\vip-users.json -WhatIf
Read-Host “Press Enter to proceed with VIP migration”
.\Import-MailboxAliases.ps1 -JsonPath .\vip-users.json

Example 3: Database-by-Database Migration

Process each mailbox database separately for better control and monitoring.

# Get list of all databases
$databases = Get-MailboxDatabase | Select-Object Name, ServerWrite-Host "Found $($databases.Count) mailbox databases:"
$databases | Format-Table

foreach ($db in $databases) {
$outputFile = “.\export-$($db.Name -replace ‘ ‘,’-‘ -replace ‘[^a-zA-Z0-9-]’,”).json”
Write-Host “Processing database: $($db.Name)” -ForegroundColor Green

# Export database
.\Export-OnPremiseMailboxes.ps1 -Database $db.Name -OutputPath $outputFile

# Review export
$dbData = Get-Content $outputFile | ConvertFrom-Json
Write-Host ” Exported $($dbData.Mailboxes.Count) mailboxes”

# Test import
Write-Host ” Testing import…” -ForegroundColor Yellow
.\Import-MailboxAliases.ps1 -JsonPath $outputFile -WhatIf

# Prompt for confirmation
$confirm = Read-Host ” Proceed with import for $($db.Name)? (Y/N)”
if ($confirm -eq ‘Y’ -or $confirm -eq ‘y’) {
Write-Host ” Importing aliases…” -ForegroundColor Green
.\Import-MailboxAliases.ps1 -JsonPath $outputFile
Write-Host ” Completed: $($db.Name)” -ForegroundColor Green
} else {
Write-Host ” Skipped: $($db.Name)” -ForegroundColor Yellow
}

Write-Host “”
}

Example 4: Selective Alias Migration

Filter and migrate only mailboxes that have aliases, reducing processing time.

# Export all mailboxes
.\Export-OnPremiseMailboxes.ps1 -OutputPath ".\all-mailboxes.json"# Filter to only include mailboxes with aliases
$allData = Get-Content .\all-mailboxes.json | ConvertFrom-Json
$filteredMailboxes = $allData.Mailboxes | Where-Object { $_.EmailAliases.Count -gt 0 }

Write-Host “Original mailboxes: $($allData.Mailboxes.Count)”
Write-Host “Mailboxes with aliases: $($filteredMailboxes.Count)”

# Create filtered export
$filteredData = @{
ExportInfo = $allData.ExportInfo
Mailboxes = $filteredMailboxes
}
$filteredData.ExportInfo.TotalMailboxes = $filteredMailboxes.Count
$filteredData.ExportInfo.Filter = “Mailboxes with aliases only”

$filteredData | ConvertTo-Json -Depth 10 | Out-File .\filtered-mailboxes.json

# Import filtered data
.\Import-MailboxAliases.ps1 -JsonPath .\filtered-mailboxes.json -WhatIf
.\Import-MailboxAliases.ps1 -JsonPath .\filtered-mailboxes.json

Troubleshooting Guide

Exchange Connection Issues

Symptoms: “Cannot connect to Exchange server”, “Access denied” errors, connection timeouts

Solutions:

# Test basic connectivity
Test-NetConnection -ComputerName "your-exchange-server.domain.com" -Port 80
Test-NetConnection -ComputerName "your-exchange-server.domain.com" -Port 443# Verify Exchange Management Shell
Get-PSSession | Where-Object {$_.ConfigurationName -eq "Microsoft.Exchange"}

# Test Exchange cmdlets
Get-ExchangeServer
Get-MailboxDatabase | Select Name, Server

# Try explicit server connection
.\Export-OnPremiseMailboxes.ps1 -Server “EXCH01.contoso.com”

# Use alternative credentials
$cred = Get-Credential -Message “Enter Exchange Admin Credentials”
.\Export-OnPremiseMailboxes.ps1 -Credential $cred

Exchange Online Module Issues

Symptoms: “ExchangeOnlineManagement module not found”, authentication failures, MFA-related errors

Solutions:

# Check current module version
Get-Module -ListAvailable -Name ExchangeOnlineManagement# Uninstall and reinstall module
Uninstall-Module -Name ExchangeOnlineManagement -Force -AllVersions
Install-Module -Name ExchangeOnlineManagement -Force -AllowClobber

# Clear cached credentials
Disconnect-ExchangeOnline -Confirm:$false

# Manual connection test
Connect-ExchangeOnline -ShowProgress $true

# Check for multiple versions
Get-Module -ListAvailable -Name ExchangeOnlineManagement | Format-Table Name, Version, Path

Mailbox Matching Problems

Symptoms: “Mailbox not found in Exchange Online”, many mailboxes skipped during import, primary SMTP address mismatches

Solutions:

# Verify mailbox exists with different address format
Get-Mailbox -Identity "[email protected]" -ErrorAction SilentlyContinue# Check accepted domains in Exchange Online
Get-AcceptedDomain | Format-Table Name, DomainName, DomainType

# List sample mailboxes for comparison
Get-Mailbox -ResultSize 10 | Select DisplayName, PrimarySmtpAddress, EmailAddresses

# Check for address policy differences
Get-EmailAddressPolicy | Format-Table Name, EnabledEmailAddressTemplates

# Verify domain routing
Get-RemoteDomain | Format-Table DomainName, IsInternal

Permission and Access Issues

Symptoms: “Insufficient permissions” errors, “Access denied” during import, partial data exports

Solutions:

# Check current user permissions in Exchange Online
Get-RoleGroupMember "Organization Management" | Where-Object {$_.Name -like "*$env:USERNAME*"}# Verify Exchange Online admin roles
Get-MsolRole | Where-Object {$_.Name -like "*Exchange*"}
Get-MsolRoleMember -RoleObjectId (Get-MsolRole -RoleName "Exchange Service Administrator").ObjectId

# Test with Global Admin account
$globalAdminCred = Get-Credential -Message “Enter Global Admin Credentials”
.\Import-MailboxAliases.ps1 -JsonPath .\export.json -Credential $globalAdminCred

# Check on-premise Exchange permissions
Get-ManagementRoleAssignment -RoleAssignee $env:USERNAME | Select Role, RoleAssigneeName

Performance and Scale Issues

Symptoms: Script timeouts with large exports, memory issues, slow processing, throttling errors

Solutions:

# Process in smaller batches
$databases = Get-MailboxDatabase
foreach ($db in $databases) {
.\Export-OnPremiseMailboxes.ps1 -Database $db.Name -OutputPath ".\batch-$($db.Name -replace ' ','-').json"
}# Use more specific filters to reduce dataset size
.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Sales' -and Office -eq 'NYC'"

# Monitor memory usage during execution
Get-Process PowerShell | Select ProcessName, WorkingSet, VirtualMemorySize

# Check for Exchange Online throttling
# Look for "Request was throttled" messages in output

# Implement delays between operations
Start-Sleep -Seconds 2 # Add between batches

JSON File Issues

Symptoms: “Invalid JSON format”, “Cannot parse JSON file”, encoding issues

Solutions:

# Validate JSON file structure

try {
$testData = Get-Content .\mailbox-addresses.json | ConvertFrom-Json
Write-Host "JSON file is valid. Contains $($testData.Mailboxes.Count) mailboxes."
} catch {
Write-Error "JSON file is invalid: $($_.Exception.Message)"
}# Check file encoding
Get-Content .\mailbox-addresses.json -Encoding UTF8 | Select-Object -First 5
# Repair JSON file if needed
$content = Get-Content .\mailbox-addresses.json -Raw
$content = $content -replace '[^\x20-\x7E\x0A\x0D]', '' # Remove non-printable characters
$content | Out-File .\mailbox-addresses-fixed.json -Encoding UTF8

# Verify file size and structure
Get-ChildItem .\mailbox-addresses.json | Select Name, Length

Best Practices

Pre-Migration Planning

Environment Assessment

  • Document current Exchange topology
  • Identify all accepted domains
  • Map organizational units and databases
  • Review existing alias patterns
  • Identify VIP users and critical mailboxes

Pilot Testing

  • Start with 5-10 test users
  • Test different mailbox types (regular, shared, distribution)
  • Verify alias functionality after migration
  • Document any issues encountered
  • Validate email delivery to migrated aliases

Backup Strategy

  • Export all mailbox data before starting
  • Keep multiple copies of JSON files
  • Document current Exchange Online configuration
  • Plan rollback procedures
  • Archive migration logs and reports

Migration Execution

1

Phased Approach

  • Migrate by department or business unit
  • Process VIP users first
  • Allow time between phases for validation
  • Monitor Exchange Online service health
2

Monitoring and Validation

  • Use WhatIf mode extensively
  • Spot-check migrated aliases
  • Test email delivery to new aliases
  • Monitor for any delivery issues
3

Communication

  • Notify users of migration schedule
  • Provide timeline for alias availability
  • Document any temporary limitations
  • Establish support procedures

Security Considerations

Security Best Practices:

  • Credential Management: Use secure credential storage, implement least-privilege access
  • Data Protection: Encrypt JSON files, limit access to migration files, use secure transfer methods
  • Audit and Compliance: Log all activities, maintain audit trails, ensure regulatory compliance
  • Access Control: Limit script access to authorized personnel, monitor admin account usage

Performance Optimization

# Optimize for large environments
# 1. Use specific filters to reduce dataset size
.\Export-OnPremiseMailboxes.ps1 -Filter "Department -eq 'Sales'" -OutputPath ".\sales-only.json"# 2. Process during off-peak hours
$currentHour = (Get-Date).Hour
if ($currentHour -lt 6 -or $currentHour -gt 22) {
Write-Host "Running during off-peak hours" -ForegroundColor Green
# Run migration
} else {
Write-Warning "Consider running during off-peak hours (10 PM - 6 AM)"
}

# 3. Implement batch processing with delays
$batchSize = 50
$mailboxes = $data.Mailboxes
for ($i = 0; $i -lt $mailboxes.Count; $i += $batchSize) {
$batch = $mailboxes[$i..($i + $batchSize – 1)]
# Process batch
Start-Sleep -Seconds 30 # Delay between batches
}

# 4. Monitor throttling and adjust accordingly
# Watch for “Request was throttled” messages and increase delays

Key Takeaways

  • Always test first: Use WhatIf mode and start with small pilot groups
  • Plan in phases: Don’t migrate everything at once – use a phased approach
  • Monitor closely: Watch for errors, throttling, and delivery issues
  • Keep backups: Maintain copies of all export files and migration logs
  • Validate results: Test email delivery to migrated aliases

⚠️ Important Disclaimers

  • Testing Required: Always test in a non-production environment first
  • Additive Only: These scripts only ADD aliases, they never remove existing ones
  • Backup Essential: Keep backups of JSON export files and current configurations
  • Service Limits: Monitor Exchange Online throttling and service limits
  • Community Support: This is a community-supported project, not an official Microsoft tool