Skip to content

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”

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.


FeatureCapabilityBusiness Value
Distribution Group SupportHandles MailUniversalDistributionGroup and MailUniversalSecurityGroupCovers all distribution group types
Hybrid API IntegrationCombines Exchange Online and Microsoft Graph APIsComprehensive data access
User Caching SystemImplements user information caching for performanceOptimizes large-scale migrations
ImmutableId MatchingUses OnPremisesImmutableId for cross-tenant user matchingEnsures accurate user identification

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

Terminal window
$backupFolder = "$home\Documents\GroupsBackup"
$sourceTenantDomain = ""
$sourceTenantDomainName = @("", "oXXXclXud.works")
$destinationTenantDomain = ""
$destinationTenantDomainName = ""

Terminal window
# 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 file
function 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-Groups

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

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

  1. Connect to Source Tenant (Exchange Online + Microsoft Graph)
  2. Filter Distribution Groups (cloud-only, mail-enabled)
  3. Export Group Information to XML files
  4. Cache User Data for performance optimization
  1. Process XML Files for domain references
  2. Replace Tenant Keywords with destination values
  3. Validate Data Integrity after conversion
  1. Connect to Destination Tenant with both APIs
  2. Create Missing Groups using hash table conversion
  3. Match Users by OnPremisesImmutableId
  4. Sync Members and Owners with conflict resolution

File TypeNaming ConventionContent
Group Info{Guid}_{DisplayName}_Info.xmlDistribution group properties
Members{Guid}_{DisplayName}_members.xmlMember list with OnPremisesImmutableId
Owners{Guid}_{DisplayName}_Owner.xmlOwner list with OnPremisesImmutableId

  • Preserve distribution lists during tenant transitions
  • Maintain email routing configurations
  • Transfer group-based permissions
  • Merge multiple Exchange environments
  • Preserve communication structures
  • Maintain business continuity
  • 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.