Script to Backup and Restore Distribution Group Members and Owners Using ImmutableId in Azure AD
Azure AD Distribution Group Backup and Restore Script
Section titled “Azure AD Distribution Group Backup and Restore Script”Overview
Section titled “Overview”This advanced script enables comprehensive backup and restoration of Azure AD distribution group members and owners using ImmutableId as the primary matching mechanism. Designed for cross-tenant migrations, it leverages both Exchange Online and Microsoft Graph APIs to ensure complete distribution group structure preservation and accurate user mapping across different environments.
Key Features
Section titled “Key Features”| Feature | Capability | Business Value |
|---|---|---|
| Distribution Group Support | Handles MailUniversalDistributionGroup and MailUniversalSecurityGroup | Covers all distribution group types |
| Hybrid API Integration | Combines Exchange Online and Microsoft Graph APIs | Comprehensive data access |
| User Caching System | Implements user information caching for performance | Optimizes large-scale migrations |
| ImmutableId Matching | Uses OnPremisesImmutableId for cross-tenant user matching | Ensures accurate user identification |
Script Requirements
Section titled “Script Requirements”Ideal for: Exchange administrators, migration specialists, and enterprise architects
Prerequisites:
- Exchange Online PowerShell Module installed
- Microsoft Graph PowerShell SDK installed
- Exchange Administrator and Global Administrator roles
- User synchronization with OnPremisesImmutableId preservation
Configuration
Section titled “Configuration”Tenant Settings
Section titled “Tenant Settings”$backupFolder = "$home\Documents\GroupsBackup"$sourceTenantDomain = ""$sourceTenantDomainName = @("", "oXXXclXud.works")$destinationTenantDomain = ""$destinationTenantDomainName = ""Implementation
Section titled “Implementation”# This script restore the members by ImmutableId!!!$backupFolder = "$home\Documents\GroupsBackup"
# Source and destination tenant domain names$sourceTenantDomain = ""$sourceTenantDomainName =@("", "oXXXclXud.works")$destinationTenantDomain = ""$destinationTenantDomainName = ""
function New-DirectoryIfNotExist { param( [string]$Path )
if (Test-Path -Path $Path -PathType Container) { $FolderProperty = Get-ItemProperty $Path $date = ($FolderProperty.CreationTime.ToString()).Split('/') $date += $date[2].Split(' ') $date += $date[4].Replace(':', '')
$suffix = '-' + $date[3] + '-' + $date[1] + '-' + $date[5] $NewName = 'GroupsBackup' + $suffix Rename-Item -Path $Path -NewName $NewName }
New-Item -ItemType Directory -Path $Path | Out-Null Write-Output "Directory '$Path' created."}
function Get-UserInfo ($UserId) { $User = $Script:UsersCache | Where-Object {$_.Id -eq $UserId} if ($null -eq $User) { $User = Get-MgUser -UserId $UserId -Property Id,UserPrincipalName,OnPremisesImmutableId | Select-Object Id,UserPrincipalName,OnPremisesImmutableId $Script:UsersCache += $User } return $User}
function Backup-Groups { $Script:UsersCache = @() New-DirectoryIfNotExist -Path $backupFolder
try { Connect-ExchangeOnline -Organization $sourceTenantDomain -ErrorAction Stop Connect-MgGraph -TenantId "$sourceTenantDomain" -Scopes "User.Read.All","User.ReadBasic.All","Directory.Read.All" -ErrorAction Stop } catch { Write-Output "Failed to connect to the source tenant. Error: $($_.Exception.Message)" return }
try { $allGroups = Get-DistributionGroup | Where-Object {$false -eq $_.IsDirSynced -and ("MailUniversalDistributionGroup" -eq $_.RecipientTypeDetails -or "MailUniversalSecurityGroup" -eq $_.RecipientTypeDetails)} } catch { Write-Output "Failed to retrieve groups. Error: $($_.Exception.Message)" return }
foreach ($sourceGroup in $allGroups) { Write-Output "Backing up $($sourceGroup.DisplayName)" try { $sourceGroupMembers = @() $GroupMembers = Get-DistributionGroupMember -Identity $($sourceGroup.Guid) | Where-Object RecipientType -eq "UserMailbox" | Select-Object -ExpandProperty ExternalDirectoryObjectId -ErrorAction SilentlyContinue if ($GroupMembers){ $GroupMembers | ForEach-Object { $sourceGroupMembers += Get-UserInfo -UserId $_ } }
if ($sourceGroup.ManagedBy){ $sourceGroupOwner = @() foreach ($GroupOwner in $($sourceGroup.ManagedBy)) { $sourceGroupOwner += Get-UserInfo -UserId $GroupOwner } }
$exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Guid)_$($sourceGroup.DisplayName)"
$xmlPath = $exportPath + '_Info.xml' $sourceGroup | Export-Clixml -Path $xmlPath
$xmlPath = $exportPath + '_members.xml' $sourceGroupMembers | Export-Clixml -Path $xmlPath
$xmlPath = $exportPath + '_Owner.xml' $sourceGroupOwner | Export-Clixml -Path $xmlPath } catch { Write-Output "Failed to backup group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)" } }}
function Set-HashTableData ($Object){ $hash = @{} $Object.psobject.properties | ForEach-Object {$hash[$_.Name] = $_.Value } return $hash}
function Restore-Groups { $Script:UsersCache = @() if (Test-Path -Path $backupFolder -PathType Container) { $AllGroups = @() $xmlFiles = Get-ChildItem -Path $backupFolder
$xmlFiles | Where-Object Name -Like "*_Info.xml" | ForEach-Object { $AllGroups += Import-Clixml -Path $_.FullName }
try { Connect-ExchangeOnline -Organization $destinationTenantDomain -ErrorAction Stop Connect-MgGraph -TenantId "$destinationTenantDomain" -Scopes "User.Read.All","User.ReadBasic.All","Directory.Read.All" -ErrorAction Stop } catch { Write-Output "Failed to connect to the destination tenant. Error: $($_.Exception.Message)" return }
$GroupsInfo = @()
foreach ($sourceGroup in $AllGroups) { try { $destinationGroup = Get-DistributionGroup -Filter "Alias -eq '$($sourceGroup.Alias)'" -ErrorAction SilentlyContinue
if ($null -eq $destinationGroup) { if ($($sourceGroup.GroupType) -Like "*SecurityEnabled*"){ $sourceGroup | Add-Member -Name 'Type' -Value 'Security' -MemberType NoteProperty }
$NewGroupHash = Set-HashTableData -Object ($sourceGroup |Select-Object Alias,Name,BccBlocked,BypassNestedModerationEnabled,CopyOwnerToMember,Description,DisplayName,MemberDepartRestriction,MemberJoinRestriction,ModeratedBy,ModerationEnabled,Notes,PrimarySmtpAddress,RequireSenderAuthenticationEnabled,SendModerationNotifications,Type) Write-Output "Creating new group $($sourceGroup.Alias)" $destinationGroup = New-DistributionGroup @NewGroupHash -ErrorAction Stop }
$GroupsInfo += @{ sourceGroupID = $($sourceGroup.Guid); GroupDisplayName = $($sourceGroup.DisplayName); destinationGroupID = $($destinationGroup.Guid) }
$exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Guid)_$($sourceGroup.DisplayName)" $xmlPath = $exportPath + '_members.xml' $sourceGroupMembers = Import-Clixml -Path $xmlPath $destinationGroupMembers = @() $GroupMembers = Get-DistributionGroupMember -Identity $destinationGroup.Guid | Where-Object RecipientType -eq "UserMailbox" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ExternalDirectoryObjectId if ($GroupMembers){ $GroupMembers | ForEach-Object { $destinationGroupMembers += Get-UserInfo -UserId $_ } }
foreach ($sourceMember in $sourceGroupMembers) { if ($sourceMember.OnPremisesImmutableId -notin $destinationGroupMembers.OnPremisesImmutableId) { $userToAdd = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceMember.OnPremisesImmutableId)'" -Property id,UserPrincipalName
if ($userToAdd) { Add-DistributionGroupMember -Identity $destinationGroup.Guid -Member $userToAdd.UserPrincipalName } } }
foreach ($destinationMember in $destinationGroupMembers) { if ($destinationMember.OnPremisesImmutableId -notin $sourceGroupMembers.OnPremisesImmutableId) { Remove-DistributionGroupMember -Identity $destinationGroup.Guid -Member $destinationMember.UserPrincipalName } }
if($destinationGroup.ManagedBy){ $destinationGroupOwner =@() foreach ($GroupOwner in $destinationGroup.ManagedBy) { $destinationGroupOwner += Get-UserInfo -UserId $GroupOwner } }
$xmlPath = $exportPath + '_Owner.xml' $sourceGroupOwner = Import-Clixml -Path $xmlPath foreach ($sourceOwner in $sourceGroupOwner) { if ($sourceOwner.OnPremisesImmutableId -notin $destinationGroupOwner.OnPremisesImmutableId) { $userToAdd = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceOwner.OnPremisesImmutableId)'" -Property Id,UserPrincipalName if ($userToAdd) { Set-DistributionGroup -Identity $destinationGroup.Guid -ManagedBy @{Add="$($userToAdd.UserPrincipalName)"} } } }
foreach ($destinationOwner in $destinationGroupOwner) { if ($destinationOwner.ImmutableId -notin $sourceGroupOwner.ImmutableId) { $userToRemove = Get-MgUser -Filter "OnPremisesImmutableId eq '$($sourceOwner.OnPremisesImmutableId)'" -Property Id,UserPrincipalName Set-DistributionGroup -Identity $destinationGroup.Guid -ManagedBy @{Remove="$($userToRemove.UserPrincipalName)"} } }
} catch { Write-Output "Failed to restore group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)" } }
$path = Join-Path -Path $backupFolder -ChildPath ".GroupsInfo.csv" $GroupsInfo | Export-Csv -Path $path }}
# Function to find and replace keywords in XML filefunction FindAndReplaceKeywordsInXML { param ( [string]$FilePath, $Keywords )
# Read the content of the XML file $content = Get-Content -Path $FilePath -Raw
# Check if the file contains any of the specified keywords $foundKeywords = $Keywords | Where-Object { $content -match $_ }
if ($foundKeywords.Count -gt 0) { # Replace the keywords with "I_Find_U" $content = $content -replace ($Keywords -join '|'), "$destinationTenantDomainName"
# Save the modified content back to the same file $content | Set-Content -Path $FilePath Write-Output "Keywords replaced in $FilePath" } else { Write-Output "No keywords found in $FilePath" }}
function Convert-Domains { # Set the folder path where XML files are located $FolderPath = $backupFolder
# Set the keywords to search for $Keywords = $sourceTenantDomainName
# Get a list of all XML files in the folder $xmlFiles = Get-ChildItem -Path $FolderPath -Filter "*.xml" -File
# Process each XML file foreach ($file in $xmlFiles) { FindAndReplaceKeywordsInXML -FilePath $file.FullName -Keywords $Keywords }}
# Uncomment the following lines to execute the backup or restore functions# Backup-Groups# Convert-Domains# Restore-GroupsAdvanced Features
Section titled “Advanced Features”User Caching System
Section titled “User Caching System”The script implements a sophisticated caching mechanism:
- Performance Optimization: Reduces API calls for repeated user lookups
- Memory Efficiency: Stores user data in script-level cache
- Cross-Function Access: Shared cache between backup and restore operations
Hybrid API Integration
Section titled “Hybrid API Integration”Combines multiple service APIs for comprehensive coverage:
- Exchange Online: Distribution group management
- Microsoft Graph: User information and ImmutableId data
- Synchronized Operations: Ensures data consistency across services
Migration Process
Section titled “Migration Process”Phase 1: Backup
Section titled “Phase 1: Backup”- Connect to Source Tenant (Exchange Online + Microsoft Graph)
- Filter Distribution Groups (cloud-only, mail-enabled)
- Export Group Information to XML files
- Cache User Data for performance optimization
Phase 2: Domain Conversion
Section titled “Phase 2: Domain Conversion”- Process XML Files for domain references
- Replace Tenant Keywords with destination values
- Validate Data Integrity after conversion
Phase 3: Restore
Section titled “Phase 3: Restore”- Connect to Destination Tenant with both APIs
- Create Missing Groups using hash table conversion
- Match Users by OnPremisesImmutableId
- Sync Members and Owners with conflict resolution
File Structure
Section titled “File Structure”Backup Files Generated
Section titled “Backup Files Generated”| File Type | Naming Convention | Content |
|---|---|---|
| Group Info | {Guid}_{DisplayName}_Info.xml | Distribution group properties |
| Members | {Guid}_{DisplayName}_members.xml | Member list with OnPremisesImmutableId |
| Owners | {Guid}_{DisplayName}_Owner.xml | Owner list with OnPremisesImmutableId |
Usage Scenarios
Section titled “Usage Scenarios”Exchange Online Migrations
Section titled “Exchange Online Migrations”- Preserve distribution lists during tenant transitions
- Maintain email routing configurations
- Transfer group-based permissions
Cross-Tenant Consolidation
Section titled “Cross-Tenant Consolidation”- Merge multiple Exchange environments
- Preserve communication structures
- Maintain business continuity
Compliance and Governance
Section titled “Compliance and Governance”- Document group membership for audits
- Backup critical distribution lists
- Enable disaster recovery scenarios
Key Takeaway: This script provides enterprise-grade distribution group migration capabilities, combining Exchange Online and Microsoft Graph APIs to ensure complete preservation of communication structures and access controls during complex tenant transitions.