Anavem
Languagefr
How to Migrate Distribution Groups from On-Premises Exchange to Microsoft 365

How to Migrate Distribution Groups from On-Premises Exchange to Microsoft 365

Learn to migrate distribution groups from on-premises Exchange to Microsoft 365 using PowerShell automation, including export, recreation, membership management, and mail flow verification.

April 13, 2026 18 min
mediumexchange 8 steps 18 min

Why Migrate Distribution Groups from On-Premises to Microsoft 365?

Distribution groups are essential for email communication in organizations, but managing them across hybrid environments creates complexity and administrative overhead. When you migrate from on-premises Exchange to Microsoft 365, distribution groups don't automatically transfer—they must be recreated in the cloud environment.

Unlike mailbox migrations that have dedicated tools, distribution group migration requires a manual process using PowerShell automation. This is because distribution groups contain complex membership data, permissions, and mail flow rules that need careful handling to maintain functionality.

What Makes Distribution Group Migration Challenging?

The main challenge is that Microsoft doesn't provide a direct migration tool for distribution groups. You're essentially rebuilding your group structure in Exchange Online while preserving all memberships, permissions, and mail routing configurations. Additionally, in hybrid environments, you must prevent synchronization conflicts between on-premises and cloud objects.

This tutorial provides a complete PowerShell-based solution that exports your on-premises distribution groups, recreates them in Exchange Online with proper prefixing to avoid conflicts, and includes optional upgrade paths to Microsoft 365 Groups for enhanced collaboration features. You'll also learn how to handle hybrid sync issues and verify mail flow after migration.

Implementation Guide

Full Procedure

01

Install and Connect to Exchange Online PowerShell

First, install the latest Exchange Online PowerShell V3 module on your management workstation. This module provides the cmdlets needed to manage Exchange Online distribution groups.

Install-Module -Name ExchangeOnlineManagement -Force -AllowClobber

Connect to Exchange Online using your admin credentials:

Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com

Verify the connection by listing existing distribution groups:

Get-DistributionGroup | Select-Object Name, PrimarySmtpAddress | Format-Table
Pro tip: Use -ShowProgress $true parameter with Connect-ExchangeOnline to see connection status in real-time.
02

Export On-Premises Distribution Groups to CSV

Connect to your on-premises Exchange server using Exchange Management Shell. Run this as an administrator on the Exchange server or a machine with Exchange management tools installed.

Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn

Create a comprehensive export script that captures all distribution group details and members:

# Create export directory
New-Item -Path "C:\Scripts" -ItemType Directory -Force

# Export distribution groups with all details
Get-DistributionGroup | ForEach-Object {
    $Group = $_
    $Members = Get-DistributionGroupMember -Identity $Group.Name
    
    foreach ($Member in $Members) {
        [PSCustomObject]@{
            GroupName = $Group.DisplayName
            GroupAlias = $Group.Alias
            PrimarySmtpAddress = $Group.PrimarySmtpAddress
            MemberName = $Member.DisplayName
            MemberEmail = $Member.PrimarySmtpAddress
            MemberType = $Member.RecipientType
            ManagedBy = ($Group.ManagedBy -join ";")
            HiddenFromAddressLists = $Group.HiddenFromAddressListsEnabled
            RequireSenderAuthenticationEnabled = $Group.RequireSenderAuthenticationEnabled
            AcceptMessagesOnlyFrom = ($Group.AcceptMessagesOnlyFrom -join ";")
        }
    }
} | Export-Csv -Path "C:\Scripts\OnPremDGs.csv" -NoTypeInformation

Verify the export by checking the CSV file:

Import-Csv "C:\Scripts\OnPremDGs.csv" | Select-Object GroupName, GroupAlias, MemberEmail | Format-Table
Warning: This export includes nested groups and security groups. You'll need to manually review and flatten nested memberships before importing to Exchange Online.
03

Prepare CSV Data for Cloud Recreation

Open the exported CSV file and prepare it for cloud import. Add a "C-" prefix to group names and aliases to avoid conflicts with existing on-premises objects during hybrid sync.

# Load and modify the CSV data
$DGData = Import-Csv "C:\Scripts\OnPremDGs.csv"

# Create modified data with cloud prefixes
$CloudDGData = $DGData | ForEach-Object {
    [PSCustomObject]@{
        GroupName = "C-" + $_.GroupName
        GroupAlias = "C-" + $_.GroupAlias
        PrimarySmtpAddress = "C-" + $_.PrimarySmtpAddress
        MemberEmail = $_.MemberEmail
        ManagedBy = $_.ManagedBy
        HiddenFromAddressLists = $_.HiddenFromAddressLists
        RequireSenderAuthenticationEnabled = $_.RequireSenderAuthenticationEnabled
        AcceptMessagesOnlyFrom = $_.AcceptMessagesOnlyFrom
    }
}

# Export the modified data
$CloudDGData | Export-Csv "C:\Scripts\CloudDGs.csv" -NoTypeInformation

Validate that all member email addresses exist in Exchange Online:

# Check if members exist in Exchange Online
$UniqueMembers = $CloudDGData | Select-Object -ExpandProperty MemberEmail | Sort-Object -Unique

foreach ($Member in $UniqueMembers) {
    try {
        $Recipient = Get-Recipient $Member -ErrorAction Stop
        Write-Host "✓ Found: $Member" -ForegroundColor Green
    }
    catch {
        Write-Host "✗ Missing: $Member" -ForegroundColor Red
    }
}

Review the output and remove any missing members from your CSV before proceeding.

04

Create Distribution Groups in Exchange Online

Import the prepared CSV and create distribution groups in Exchange Online. This script creates groups without members first, then adds members in the next step.

# Import the cloud-ready CSV
$CloudDGs = Import-Csv "C:\Scripts\CloudDGs.csv"

# Get unique groups (since CSV has one row per member)
$UniqueGroups = $CloudDGs | Group-Object GroupName | ForEach-Object {
    $_.Group[0]  # Take first occurrence of each group
}

# Create distribution groups
foreach ($Group in $UniqueGroups) {
    try {
        Write-Host "Creating group: $($Group.GroupName)" -ForegroundColor Yellow
        
        $NewDGParams = @{
            Name = $Group.GroupName
            Alias = $Group.GroupAlias
            PrimarySmtpAddress = $Group.PrimarySmtpAddress
            Type = "Distribution"
        }
        
        # Add optional parameters if they exist
        if ($Group.ManagedBy) {
            $Managers = $Group.ManagedBy -split ";"
            $NewDGParams.ManagedBy = $Managers
        }
        
        New-DistributionGroup @NewDGParams
        
        # Configure additional settings
        Set-DistributionGroup -Identity $Group.GroupAlias -HiddenFromAddressListsEnabled ([bool]::Parse($Group.HiddenFromAddressLists))
        Set-DistributionGroup -Identity $Group.GroupAlias -RequireSenderAuthenticationEnabled ([bool]::Parse($Group.RequireSenderAuthenticationEnabled))
        
        Write-Host "✓ Created: $($Group.GroupName)" -ForegroundColor Green
    }
    catch {
        Write-Host "✗ Failed to create $($Group.GroupName): $($_.Exception.Message)" -ForegroundColor Red
    }
}

Verify group creation in Exchange Admin Center or PowerShell:

Get-DistributionGroup -Filter "Name -like 'C-*'" | Select-Object Name, PrimarySmtpAddress, ManagedBy | Format-Table
Pro tip: Use -WhatIf parameter with New-DistributionGroup to test the script without actually creating groups first.
05

Add Members to Distribution Groups

Now add all members to their respective distribution groups. This step processes the CSV to add each member to the correct group.

# Group members by distribution group
$GroupedMembers = $CloudDGs | Group-Object GroupAlias

foreach ($GroupData in $GroupedMembers) {
    $GroupAlias = $GroupData.Name
    $Members = $GroupData.Group
    
    Write-Host "Adding members to group: $GroupAlias" -ForegroundColor Yellow
    
    foreach ($Member in $Members) {
        try {
            # Verify member exists before adding
            $Recipient = Get-Recipient $Member.MemberEmail -ErrorAction Stop
            
            Add-DistributionGroupMember -Identity $GroupAlias -Member $Member.MemberEmail -ErrorAction Stop
            Write-Host "  ✓ Added: $($Member.MemberEmail)" -ForegroundColor Green
        }
        catch [Microsoft.Exchange.Management.RecipientTasks.MemberAlreadyExistsException] {
            Write-Host "  ⚠ Already member: $($Member.MemberEmail)" -ForegroundColor Yellow
        }
        catch {
            Write-Host "  ✗ Failed to add $($Member.MemberEmail): $($_.Exception.Message)" -ForegroundColor Red
        }
    }
}

Verify membership for a few groups:

# Check membership for first few groups
Get-DistributionGroup -Filter "Name -like 'C-*'" | Select-Object -First 3 | ForEach-Object {
    Write-Host "\nMembers of $($_.Name):" -ForegroundColor Cyan
    Get-DistributionGroupMember $_.Name | Select-Object Name, PrimarySmtpAddress | Format-Table
}

Generate a membership report:

# Create membership report
$MembershipReport = Get-DistributionGroup -Filter "Name -like 'C-*'" | ForEach-Object {
    $Group = $_
    $MemberCount = (Get-DistributionGroupMember $Group.Name).Count
    
    [PSCustomObject]@{
        GroupName = $Group.Name
        PrimarySmtpAddress = $Group.PrimarySmtpAddress
        MemberCount = $MemberCount
        ManagedBy = ($Group.ManagedBy -join ";")
    }
}

$MembershipReport | Export-Csv "C:\Scripts\MigrationReport.csv" -NoTypeInformation
$MembershipReport | Format-Table
06

Configure Mail Flow and Test Distribution Groups

Configure mail flow settings and test the migrated distribution groups to ensure they work correctly. This includes setting up mail routing and testing message delivery.

# Configure mail flow settings for migrated groups
Get-DistributionGroup -Filter "Name -like 'C-*'" | ForEach-Object {
    $Group = $_
    
    # Ensure proper mail routing
    Set-DistributionGroup -Identity $Group.Name -EmailAddressPolicyEnabled $false
    
    # Add original SMTP address as secondary if needed
    $OriginalAddress = $Group.PrimarySmtpAddress -replace "^C-", ""
    
    try {
        Set-DistributionGroup -Identity $Group.Name -EmailAddresses @{Add="smtp:$OriginalAddress"}
        Write-Host "✓ Added secondary address $OriginalAddress to $($Group.Name)" -ForegroundColor Green
    }
    catch {
        Write-Host "⚠ Could not add secondary address for $($Group.Name): $($_.Exception.Message)" -ForegroundColor Yellow
    }
}

Test mail delivery to the distribution groups:

# Test mail flow (replace with your test addresses)
$TestGroups = Get-DistributionGroup -Filter "Name -like 'C-*'" | Select-Object -First 2

foreach ($TestGroup in $TestGroups) {
    Write-Host "Testing mail flow for: $($TestGroup.Name)" -ForegroundColor Yellow
    
    # Send test message
    $TestMessage = @{
        To = $TestGroup.PrimarySmtpAddress
        Subject = "Test message for $($TestGroup.Name) - $(Get-Date)"
        Body = "This is a test message to verify distribution group functionality."
        From = "admin@yourdomain.com"  # Replace with your admin email
    }
    
    try {
        Send-MailMessage @TestMessage -SmtpServer "outlook.office365.com" -Port 587 -UseSsl -Credential (Get-Credential)
        Write-Host "✓ Test message sent to $($TestGroup.PrimarySmtpAddress)" -ForegroundColor Green
    }
    catch {
        Write-Host "✗ Failed to send test message: $($_.Exception.Message)" -ForegroundColor Red
    }
}

Monitor message trace for delivery confirmation:

# Check message trace (wait 5-10 minutes after sending)
$StartDate = (Get-Date).AddHours(-1)
$EndDate = Get-Date

Get-MessageTrace -StartDate $StartDate -EndDate $EndDate -RecipientAddress "C-*@yourdomain.com" | 
    Select-Object Received, SenderAddress, RecipientAddress, Subject, Status | 
    Format-Table -AutoSize
Warning: Mail flow can take 5-10 minutes to propagate. Don't panic if test messages don't arrive immediately.
07

Handle Hybrid Sync and Clean Up On-Premises Groups

If you're in a hybrid environment, you need to prevent sync conflicts and clean up the on-premises distribution groups. This step ensures the cloud groups become authoritative.

# Create a separate OU for cloud-managed distribution groups
# Run this on your domain controller or AD management server
Import-Module ActiveDirectory

# Create OU for cloud distribution groups
New-ADOrganizationalUnit -Name "CloudDistributionGroups" -Path "DC=yourdomain,DC=com" -Description "Distribution groups managed in Exchange Online"

# Move on-premises DGs to the new OU (this removes them from sync scope)
$OnPremGroups = Get-DistributionGroup | Where-Object {$_.Name -notlike "C-*"}

foreach ($Group in $OnPremGroups) {
    try {
        # Get the AD object
        $ADGroup = Get-ADGroup -Filter "Name -eq '$($Group.Name)'"
        
        if ($ADGroup) {
            Move-ADObject -Identity $ADGroup.DistinguishedName -TargetPath "OU=CloudDistributionGroups,DC=yourdomain,DC=com"
            Write-Host "✓ Moved $($Group.Name) to CloudDistributionGroups OU" -ForegroundColor Green
        }
    }
    catch {
        Write-Host "✗ Failed to move $($Group.Name): $($_.Exception.Message)" -ForegroundColor Red
    }
}

Update Microsoft Entra Connect sync configuration to exclude the new OU:

# Configure Entra Connect to exclude the CloudDistributionGroups OU
# This prevents sync conflicts between on-premises and cloud groups

# Note: This requires running the Entra Connect configuration wizard
# or modifying the sync rules through the Synchronization Rules Editor

Write-Host "IMPORTANT: Configure Entra Connect to exclude the CloudDistributionGroups OU from sync" -ForegroundColor Red
Write-Host "1. Open Entra Connect Synchronization Rules Editor" -ForegroundColor Yellow
Write-Host "2. Create an inbound rule to exclude OU=CloudDistributionGroups" -ForegroundColor Yellow
Write-Host "3. Run a full sync cycle after configuration" -ForegroundColor Yellow

Verify the migration is complete:

# Final verification report
$MigrationSummary = @{
    OnPremisesGroups = (Get-DistributionGroup | Where-Object {$_.Name -notlike "C-*"}).Count
    CloudGroups = (Get-DistributionGroup -Filter "Name -like 'C-*'").Count
    TotalMembers = 0
}

# Count total members in cloud groups
Get-DistributionGroup -Filter "Name -like 'C-*'" | ForEach-Object {
    $MigrationSummary.TotalMembers += (Get-DistributionGroupMember $_.Name).Count
}

Write-Host "\n=== MIGRATION SUMMARY ===" -ForegroundColor Cyan
Write-Host "On-premises groups remaining: $($MigrationSummary.OnPremisesGroups)" -ForegroundColor Yellow
Write-Host "Cloud groups created: $($MigrationSummary.CloudGroups)" -ForegroundColor Green
Write-Host "Total members migrated: $($MigrationSummary.TotalMembers)" -ForegroundColor Green

# Export final report
$MigrationSummary | ConvertTo-Json | Out-File "C:\Scripts\FinalMigrationSummary.json"
Pro tip: Keep the on-premises groups in the separate OU for 30 days before deleting them, in case you need to rollback or reference the original configuration.
08

Optional: Upgrade Distribution Groups to Microsoft 365 Groups

After successful migration, you can upgrade eligible distribution groups to Microsoft 365 Groups for enhanced collaboration features. This step is optional but recommended for groups that need modern collaboration tools.

# Check which groups are eligible for upgrade to Microsoft 365 Groups
Get-DistributionGroup -Filter "Name -like 'C-*'" | ForEach-Object {
    try {
        $EligibilityCheck = Get-EligibleDistributionGroupForMigration -Identity $_.Name
        
        [PSCustomObject]@{
            GroupName = $_.Name
            PrimarySmtpAddress = $_.PrimarySmtpAddress
            Eligible = $EligibilityCheck.IsEligibleForUpgrade
            Reasons = ($EligibilityCheck.IneligibilityReasons -join "; ")
        }
    }
    catch {
        [PSCustomObject]@{
            GroupName = $_.Name
            PrimarySmtpAddress = $_.PrimarySmtpAddress
            Eligible = $false
            Reasons = "Error checking eligibility: $($_.Exception.Message)"
        }
    }
} | Format-Table -AutoSize

Upgrade eligible groups to Microsoft 365 Groups:

# Upgrade eligible distribution groups to Microsoft 365 Groups
$EligibleGroups = Get-DistributionGroup -Filter "Name -like 'C-*'" | Where-Object {
    try {
        $Check = Get-EligibleDistributionGroupForMigration -Identity $_.Name
        return $Check.IsEligibleForUpgrade
    }
    catch {
        return $false
    }
}

foreach ($Group in $EligibleGroups) {
    try {
        Write-Host "Upgrading $($Group.Name) to Microsoft 365 Group..." -ForegroundColor Yellow
        
        # Initiate the upgrade
        Upgrade-DistributionGroup -DlIdentities $Group.PrimarySmtpAddress
        
        Write-Host "✓ Upgrade initiated for $($Group.Name)" -ForegroundColor Green
        Write-Host "  Note: Upgrade process may take several minutes to complete" -ForegroundColor Cyan
    }
    catch {
        Write-Host "✗ Failed to upgrade $($Group.Name): $($_.Exception.Message)" -ForegroundColor Red
    }
}

Monitor upgrade progress and verify Microsoft 365 Groups:

# Check upgrade status (run this after 10-15 minutes)
Write-Host "Checking for new Microsoft 365 Groups..." -ForegroundColor Yellow

# List Microsoft 365 Groups that were created from distribution groups
Get-UnifiedGroup | Where-Object {$_.DisplayName -like "C-*"} | 
    Select-Object DisplayName, PrimarySmtpAddress, WhenCreated, GroupType | 
    Format-Table -AutoSize

# Verify group features are available
Get-UnifiedGroup | Where-Object {$_.DisplayName -like "C-*"} | Select-Object -First 1 | ForEach-Object {
    Write-Host "\nFeatures available for $($_.DisplayName):" -ForegroundColor Cyan
    Write-Host "- SharePoint Site: $($_.SharePointSiteUrl)" -ForegroundColor Green
    Write-Host "- Teams Integration: Available" -ForegroundColor Green
    Write-Host "- Planner: Available" -ForegroundColor Green
    Write-Host "- Calendar: Available" -ForegroundColor Green
}

Final cleanup and documentation:

# Generate final migration and upgrade report
$FinalReport = @{
    MigrationDate = Get-Date
    DistributionGroupsMigrated = (Get-DistributionGroup -Filter "Name -like 'C-*'").Count
    Microsoft365GroupsCreated = (Get-UnifiedGroup | Where-Object {$_.DisplayName -like "C-*"}).Count
    RemainingDistributionGroups = (Get-DistributionGroup -Filter "Name -like 'C-*' -and RecipientTypeDetails -eq 'MailUniversalDistributionGroup'").Count
}

$FinalReport | ConvertTo-Json | Out-File "C:\Scripts\CompleteMigrationReport.json"

Write-Host "\n=== FINAL MIGRATION REPORT ===" -ForegroundColor Cyan
Write-Host "Migration completed on: $($FinalReport.MigrationDate)" -ForegroundColor Green
Write-Host "Distribution groups migrated: $($FinalReport.DistributionGroupsMigrated)" -ForegroundColor Green
Write-Host "Upgraded to Microsoft 365 Groups: $($FinalReport.Microsoft365GroupsCreated)" -ForegroundColor Green
Write-Host "Remaining as distribution groups: $($FinalReport.RemainingDistributionGroups)" -ForegroundColor Yellow

Write-Host "\nMigration complete! All reports saved to C:\Scripts\" -ForegroundColor Cyan
Pro tip: Microsoft 365 Groups provide additional features like SharePoint sites, Teams integration, and Planner. Consider upgrading groups that need these collaboration features.

Frequently Asked Questions

Can I migrate distribution groups directly from on-premises Exchange to Microsoft 365?+
No, there is no direct migration tool for distribution groups. Microsoft doesn't provide an automated way to migrate distribution groups from on-premises Exchange to Microsoft 365. You must export the group data using PowerShell, then recreate the groups in Exchange Online. This tutorial provides complete PowerShell scripts to automate this process, including member export, group recreation, and membership restoration.
What happens to nested distribution groups during migration to Exchange Online?+
Nested distribution groups require special handling during migration. Exchange Online supports nested groups, but the export and import process may flatten the structure. You'll need to manually review your CSV export data and recreate the nesting relationships after the initial migration. The PowerShell scripts in this tutorial capture nested memberships, but you may need to adjust the import logic for complex nested scenarios.
How do I prevent sync conflicts between on-premises and cloud distribution groups?+
To prevent sync conflicts in hybrid environments, use a naming prefix (like 'C-') for cloud groups and move on-premises groups to a separate OU that's excluded from Microsoft Entra Connect sync. This tutorial shows how to create a CloudDistributionGroups OU and configure sync exclusions. This ensures the cloud groups become authoritative without conflicts from on-premises objects.
Should I upgrade migrated distribution groups to Microsoft 365 Groups?+
Upgrading to Microsoft 365 Groups is recommended for groups that need modern collaboration features like SharePoint sites, Teams integration, and Planner. However, not all distribution groups are eligible for upgrade—security groups and groups with complex mail flow rules may not qualify. Use the Get-EligibleDistributionGroupForMigration cmdlet to check eligibility before attempting upgrades.
How long does it take for mail flow to work after migrating distribution groups?+
Mail flow typically takes 5-10 minutes to propagate after creating distribution groups in Exchange Online. DNS changes and mail routing updates need time to replicate across Microsoft's infrastructure. Test mail delivery using the message trace functionality shown in this tutorial. If mail flow doesn't work after 30 minutes, check for configuration issues like incorrect SMTP addresses or missing members.

Discussion

Share your thoughts and insights

Sign in to join the discussion