Distribution Groups stuck in Active Directory can cause issues after an Exchange migration.  

  • Users can no longer access the management of a Distribution Group in Outlook
  • Synchronized Distribution Groups in Office 365 cannot be modified in Office 365 – as a synchronized object you must update in Active Directory
  • Adding external contacts to a synchronized Distribution Group becomes difficult as you cannot synchronize contacts with Azure AD Connect

The solution is relatively simple – convert all Distribution Groups to Cloud objects.

This script was designed to do exactly that.

<#
#########################################################################################
##
## Name:        DG_Cloud.PS1
##
## Version:     1.0
##
## Description: $ Installs required components for Exchange Online Powershell Management
##              $ Creates a “Working” folder for Sea to Sky (C:\STS) for backups.
##              $ Creates an “Exports” folder for the temp files needed to migrate the
##              Distribution Lists.
##              $ Backs up the Distribution List Names and Attributes to DG_Details_Backup.csv
##              $ Backs up the Distribution List Members to DG_Members_Backup.csv
##              $ Capable of running mulitple times and retaining existing backups – creates
##              new backups each time it’s run if any new groups are detected
##              $ Selectively Creates a copy of each Distribution Group called Cloud_$Group
##              that are specifically Distribution Groups and not Mail-Enabled Security
##              groups.
##              $ Deletes the selected Distribtuion Groups from Active Directory
##              $ Initiates an Azure AD Connect to remove the AD objects from Cloud Environment
##              $ Forces wait period of 5 minutes to allow Azure AD to synchronize with Exchange
##              $ Completes process by renaming Cloud_$Group back to original name
##
## Usage:       Execute script in PowerShell with elevated privileges
##
## Author: Jason Zondag
##
## Disclaimer:  Has not been tried in every conceivable environment – always check the results
##              and fall back on the backups created to recreate the Distribution Groups if
##              necessary
##
#########################################################################################
###### ALTERNATIVE CODE FOR MFA LOGIN TO OFFICE 365  ####################################
#Connect & Login to ExchangeOnline (MFA)
$getsessions = Get-PSSession | Select-Object -Property State, Name
$isconnected = (@($getsessions) -like ‘@{State=Opened; Name=ExchangeOnlineInternalSession*’).Count -gt 0
If ($isconnected -ne “True”) {
Connect-ExchangeOnline
}
#########################################################################################
#>
clear
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host “!!!!!IMPORTANT!!!!!!” -ForeGroundColor Red
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host “!!!!!IMPORTANT!!!!!!” -ForeGroundColor Red
Write-Host “YOU MUST RUN THIS SCRIPT FROM THE DOMAIN CONTROLLER THAT IS RUNNING AZURE AD CONNECT” -ForeGroundColor Red
sleep 5
Write-Host “IF YOU ARE NOT PLEASE USE CTRL + C TO ESCAPE AND RUN FROM THE APPROPRIATE DOMAIN CONTROLLER” -ForeGroundColor Red
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host “It’s also important to note that this only affects Distribution Lists and not Mail-Enabled” -ForeGroundColor Green
Write-Host “Security Groups.  Mail-Enabled Security Groups must be handled differently.” -ForeGroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
sleep 15
Pause
Write-Host “Connecting to Exchange Online – installing all required PowerShell Modules and initiaing a connection” -ForegroundColor Green
# ————————————————————————–
# Load PowerShell Modules
# ————————————————————————–
Set-ExecutionPolicy RemoteSigned -Force
Import-Module ActiveDirectory
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-Module -Name ExchangeOnlineManagement -Force
Import-Module ExchangeOnlineManagement
#Connect & Login to ExchangeOnline (MFA)
$getsession = get-pssession | select-object -Property State | select -expandproperty state
If ($getsession -ne “Opened”) {
Connect-ExchangeOnline
}
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host
Write-host
Write-Host “______________________________________________________________________________________________” -ForegroundColor Cyan
Write-Host “Synchronized Distribution Groups with no ManagedBy settings will be defaulted to Organization” -ForeGroundColor Yellow
Write-Host “Management. This value cannot be translated.” -ForeGroundColor Yellow
Write-host
Write-Host “You must set a default account value to replace Organization Management.” -ForeGroundColor Green
Write-Host “The default account must be a valid licensed address for this tenant.  IE. seatosky@domain.com ” -ForeGroundColor Green
$ManagedByDefault = Read-host “Enter the email address of a valid licensed account for this tenant:”
Write-Host “______________________________________________________________________________________________” -ForegroundColor Cyan
# Disable Azure AD Connect from initiating a sync while this process is underway
Set-ADSyncScheduler -SyncCycleEnabled $false
Write-host “Azure AD Connect Schedule Sync has been disabled temporarily.”
# ————————————————————————–
# Create Working and Export folders
# ————————————————————————–
Write-Host “Creating a Working Directory C:\DG-Migrate and an Exports Directory within the Working Directory” -ForegroundColor Green
# Create a working directory
$orginfo = Get-OrganizationConfig | select -expandproperty Name
$WorkingDirectory = “C:\DG-Migrate\” + $orginfo + “\”
$ExportDirectory = $WorkingDirectory + “ExportedAddresses\”
If(!(Test-Path -Path $WorkingDirectory )){
# if WorkingDirectory doesn’t exist neither does ExportDirectory – create them both
            Write-Host ”  Creating Directory: $WorkingDirectory”
            New-Item -ItemType directory -Path $WorkingDirectory | Out-Null
            Write-Host ”  Creating Directory: $ExportDirectory”
            New-Item -ItemType directory -Path $ExportDirectory | Out-Null
        } else {
# WorkingDirectory may exist but that doesn’t mean ExportDirectory does – create if it doesn’t exist
If(!(Test-Path -Path $ExportDirectory )){
            Write-Host ”  Creating Directory: $ExportDirectory”
            New-Item -ItemType directory -Path $ExportDirectory | Out-Null
}
}
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host “Creating a backup of all AD Synchronized Distribution Lists and placing into the Working Directory” -ForegroundColor Green
# ————————————————————————–
# Export all the Distribution Group Information to a separate file
# ————————————————————————–
$check = (get-distributiongroup | Where {($_.IsDirSynced -eq $true) -AND ($_.RecipientType -eq “MailUniversalDistributionGroup”)})
if ((($check | Measure-Object).count) -ne 0) {
# Not 0 so we found some Distribution Groups to migrate
# We don’t want to overwrite an existing backup set – rename any existing files with a time stamp
    if (Test-Path ($WorkingDirectory + “DG_Details_Backup.csv”)) {
        $filename = ($WorkingDirectory + “DG_Details_Backup.csv”)
        $fileObj = get-item $filename
        $DateStamp = get-date -uformat “%Y-%m-%d@%H-%M-%S”
        $extOnly = $fileObj.extension
        if ($extOnly.length -eq 0) {
            $nameOnly = $fileObj.Name
            rename-item “$fileObj” “$nameOnly-$DateStamp”
            }
        else {
            $nameOnly = $fileObj.Name.Replace( $fileObj.Extension,”)
            rename-item “$fileName” “$nameOnly-$DateStamp$extOnly”
        }       }
$check | select `
    GroupType, `
    SamAccountName, `
    IsDirSynced, `
    @{label=”ManagedBy”;expression={
        ($_.managedby `
        | % { get-mailbox -identity $_ | select-object -ExpandProperty PrimarySMTPAddress } `
        | Where-Object {$_ -like “*@*”}) -join ‘;’}
        }, `
    MemberJoinRestriction, `
    MemberDepartRestriction, `
    ReportToOriginatorEnabled, `
    Description, `
    AddressListMembership, `
    Alias, `
    DisplayName, `
    PrimarySMTPAddress, `
    @{label=”EmailAddressess”;expression={
        ($_.EmailAddresses | Where-Object {$_ -like “*smtp:*” }) -join ‘;’}
        },`
    ExternalDirectoryObjectId, `
    HiddenFromAddressListsEnabled, `
    LegacyExchangeDN, `
    MaxSendSize, `
    MaxReceiveSize, `
    ModeratedBy, `
    ModerationEnabled, `
    PoliciesIncluded, `
    PoliciesExcluded, `
    EmailAddressPolicyEnabled, `
    RecipientType, `
    RecipientTypeDetials, `
    RequireSenderAuthenticationEnabled, `
    WindowsEmailAddress, `
    Identity, `
    Id, `
    Name, `
    DistinguishedName, `
    ExchangeObjectId, `
    Guid `
| Export-CSV ($WorkingDirectory + “DG_Details_Backup.csv”) -NoTypeInformation
sleep 20
    }
else {
    Write-Host “There are no appropriate Distribution Lists to migrate.  Cancelling migration.”
    Break
}
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-Host “Creating a backup of Distribution List Membership and placing in the Working Directory” -ForegroundColor Green
# ————————————————————————–
# Export all the Distribution Group Members to a separate file
# ————————————————————————–
$output = @()
$Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select Name,PrimarySmtpAddress,Managedby,GroupType,RecipientType
If ($Identities) {
Foreach($group in $Identities) {
    $Members = Get-DistributionGroupMember $group.PrimarySmtpAddress -resultsize unlimited
    if (@($Members.count) -eq 0) {
        #$managers = ($group | Select @{Name=’DistributionGroupManagers’;Expression={[string]::join(“;”, ($_.Managedby))}})
        $userObj = New-Object PSObject
        $userObj | Add-Member NoteProperty -Name “DisplayName” -Value EmptyGroup
        $userObj | Add-Member NoteProperty -Name “Alias” -Value EmptyGroup
        $userObj | Add-Member NoteProperty -Name “RecipientType” -Value EmptyGroup
        $userObj | Add-Member NoteProperty -Name “Recipient OU” -Value EmptyGroup
        $userObj | Add-Member NoteProperty -Name “Primary SMTP address” -Value EmptyGroup
        $userObj | Add-Member NoteProperty -Name “Distribution Group” -Value $group.Name
        $userObj | Add-Member NoteProperty -Name “Distribution Group Primary SMTP address” -Value $group.PrimarySmtpAddress
        $userObj | Add-Member NoteProperty -Name “Distribution Group Managers” -Value $managers.DistributionGroupManagers
        $userObj | Add-Member NoteProperty -Name “Distribution Group Type” -Value $group.GroupType
        $userObj | Add-Member NoteProperty -Name “Distribution Group Recipient Type” -Value $group.RecipientType
        $output+=$UserObj
        }
    else {
    Foreach($Member in $members) {
        #$managers = $group | Select @{Name=’DistributionGroupManagers’;Expression={[string]::join(“;”, ($_.Managedby))}}
        $userObj = New-Object PSObject
        $userObj | Add-Member NoteProperty -Name “DisplayName” -Value $Member.Name
        $userObj | Add-Member NoteProperty -Name “Alias” -Value $Member.Alias
        $userObj | Add-Member NoteProperty -Name “RecipientType” -Value $Member.RecipientType
        $userObj | Add-Member NoteProperty -Name “Recipient OU” -Value $Member.OrganizationalUnit
        $userObj | Add-Member NoteProperty -Name “Primary SMTP address” -Value $Member.PrimarySmtpAddress
        $userObj | Add-Member NoteProperty -Name “Distribution Group” -Value $group.Name
        $userObj | Add-Member NoteProperty -Name “Distribution Group Primary SMTP address” -Value $group.PrimarySmtpAddress
        $userObj | Add-Member NoteProperty -Name “Distribution Group Managers” -Value $managers.DistributionGroupManagers
        $userObj | Add-Member NoteProperty -Name “Distribution Group Type” -Value $group.GroupType
        $userObj | Add-Member NoteProperty -Name “Distribution Group Recipient Type” -Value $group.RecipientType
        $output+=$UserObj
        }
    }
   }
   # We don’t want to overwrite an existing backup set – rename any existing files with a time stamp
    if (Test-Path ($WorkingDirectory + “DG_Members_Backup.csv”)) {
        $filename = ($WorkingDirectory + “DG_Members_Backup.csv”)
        $fileObj = get-item $filename
        $DateStamp = get-date -uformat “%Y-%m-%d@%H-%M-%S”
        $extOnly = $fileObj.extension
        if ($extOnly.length -eq 0) {
            $nameOnly = $fileObj.Name
            rename-item “$fileObj” “$nameOnly-$DateStamp”
            }
        else {
            $nameOnly = $fileObj.Name.Replace( $fileObj.Extension,”)
            rename-item “$fileName” “$nameOnly-$DateStamp$extOnly”
        }       }
    $output | Export-CSV ($WorkingDirectory + “DG_Members_Backup.csv”) -NoTypeInformation
   }
# ———————————————————————————————-
sleep 15
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
# ————————————————————————–
# Create the Cloud copies of the Distribution Lists
# ————————————————————————–
Write-Host “Creating Cloud copies of each AD Synced Distribution List” -ForegroundColor Green
$Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select -expandproperty PrimarySmtpAddress
# Create the cloud versions
If ($Identities) {
    foreach ($group in $identities) {
    If (((Get-DistributionGroup $group -Resultsize Unlimited -ErrorAction ‘SilentlyContinue’).IsValid) -eq $true) {
        $OldDG = Get-DistributionGroup $group
        [System.IO.Path]::GetInvalidFileNameChars() | ForEach {$Group = $Group.Replace($_,’_’)}
        $OldName = [string]$OldDG.Name
        $OldDisplayName = [string]$OldDG.DisplayName
        $OldPrimarySmtpAddress = [string]$OldDG.PrimarySmtpAddress
        $OldAlias = [string]$OldDG.Alias
           
            if ((![string]$OldDG.managedby) -or ([string]$OldDG.managedby -eq “Organization Management”)) {
                [string]$OldDG.managedby=$ManagedByDefault
                }
           
        $OldMembers = (Get-DistributionGroupMember $OldDG.PrimarySmtpAddress).primarysmtpaddress “EmailAddress” > “$ExportDirectory\$OldName.csv”
        $OldDG.EmailAddresses >> “$ExportDirectory\$OldName.csv”
        “x500:”+$OldDG.LegacyExchangeDN >> “$ExportDirectory\$OldName.csv”
        Write-Host ”  Creating Group: Cloud-$OldDisplayName” -ForegroundColor Green
        New-DistributionGroup `
            -Name “Cloud-$OldName” `
            -Alias “Cloud-$OldAlias” `
            -DisplayName “Cloud-$OldDisplayName” `
            -ManagedBy $OldDG.ManagedBy `
            -Members $OldMembers `
            -PrimarySmtpAddress “Cloud-$OldPrimarySmtpAddress” | Out-Null
        Sleep -Seconds 3
        Write-Host ”  Setting Values For: Cloud-$OldDisplayName” -ForegroundColor Green
        Set-DistributionGroup `
            -Identity “Cloud-$OldPrimarySmtpAddress” `
            -AcceptMessagesOnlyFromSendersOrMembers $OldDG.AcceptMessagesOnlyFromSendersOrMembers `
            -RejectMessagesFromSendersOrMembers $OldDG.RejectMessagesFromSendersOrMembers `
        Set-DistributionGroup `
            -Identity “Cloud-$OldPrimarySmtpAddress” `
            -AcceptMessagesOnlyFrom $OldDG.AcceptMessagesOnlyFrom `
            -AcceptMessagesOnlyFromDLMembers $OldDG.AcceptMessagesOnlyFromDLMembers `
            -BypassModerationFromSendersOrMembers $OldDG.BypassModerationFromSendersOrMembers `
            -BypassNestedModerationEnabled $OldDG.BypassNestedModerationEnabled `
            -CustomAttribute1 $OldDG.CustomAttribute1 `
            -CustomAttribute2 $OldDG.CustomAttribute2 `
            -CustomAttribute3 $OldDG.CustomAttribute3 `
            -CustomAttribute4 $OldDG.CustomAttribute4 `
            -CustomAttribute5 $OldDG.CustomAttribute5 `
            -CustomAttribute6 $OldDG.CustomAttribute6 `
            -CustomAttribute7 $OldDG.CustomAttribute7 `
            -CustomAttribute8 $OldDG.CustomAttribute8 `
            -CustomAttribute9 $OldDG.CustomAttribute9 `
            -CustomAttribute10 $OldDG.CustomAttribute10 `
            -CustomAttribute11 $OldDG.CustomAttribute11 `
            -CustomAttribute12 $OldDG.CustomAttribute12 `
            -CustomAttribute13 $OldDG.CustomAttribute13 `
            -CustomAttribute14 $OldDG.CustomAttribute14 `
            -CustomAttribute15 $OldDG.CustomAttribute15 `
            -ExtensionCustomAttribute1 $OldDG.ExtensionCustomAttribute1 `
            -ExtensionCustomAttribute2 $OldDG.ExtensionCustomAttribute2 `
            -ExtensionCustomAttribute3 $OldDG.ExtensionCustomAttribute3 `
            -ExtensionCustomAttribute4 $OldDG.ExtensionCustomAttribute4 `
            -ExtensionCustomAttribute5 $OldDG.ExtensionCustomAttribute5 `
            -GrantSendOnBehalfTo $OldDG.GrantSendOnBehalfTo `
            -HiddenFromAddressListsEnabled $True `
            -MailTip $OldDG.MailTip `
            -MailTipTranslations $OldDG.MailTipTranslations `
            -MemberDepartRestriction $OldDG.MemberDepartRestriction `
            -MemberJoinRestriction $OldDG.MemberJoinRestriction `
            -ModeratedBy $OldDG.ModeratedBy `
            -ModerationEnabled $OldDG.ModerationEnabled `
            -RejectMessagesFrom $OldDG.RejectMessagesFrom `
            -RejectMessagesFromDLMembers $OldDG.RejectMessagesFromDLMembers `
            -ReportToManagerEnabled $OldDG.ReportToManagerEnabled `
            -ReportToOriginatorEnabled $OldDG.ReportToOriginatorEnabled `
            -RequireSenderAuthenticationEnabled $OldDG.RequireSenderAuthenticationEnabled `
            -SendModerationNotifications $OldDG.SendModerationNotifications `
            -SendOofMessageToOriginatorEnabled $OldDG.SendOofMessageToOriginatorEnabled `
            -BypassSecurityGroupManagerCheck
        sleep 3
    }                
    Else {
        Write-Host ”  ERROR: The distribution group ‘$Group’ was not found” -ForegroundColor Red
        Write-Host
    }
}
}
# ————————————————————————–
# Delete all the Distribution Groups in Active Directory
# ————————————————————————–
Write-Host “All Distribution Lists have been replicated in the Cloud with Cloud_ as a prefix” -ForegroundColor Green
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-host “If you encountered any errors during the creation of the Cloud-Group process you may hit CTRL + C now to kill the process.” -ForegroundColor Red -BackgroundColor Black
Write-host “If you kill the process now to fix any issues you should remove the Cloud-Group objects from Azure AD and start fresh.” -ForegroundColor Red -BackgroundColor Black
Write-host “WARNING – The Azure AZ Connect Sync Schedule is currently Suspended. You must complete the script or manually restart the Schedule.” -ForegroundColor Black -BackgroundColor Red
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Write-host “Press Enter to delete the migrated Distribution Lists from Active Directory” -ForegroundColor Cyan
pause
If (test-path ($WorkingDirectory + “DG_Details_Backup.csv”)) {
    $Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select -expandproperty Identity
    foreach ($group in $identities) {
        Remove-ADGroup -identity “$group” -confirm:$false
        sleep 2
        }
}
Write-Host “All Distribution Lists have been removed from Active Directory” -ForegroundColor Green
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
sleep 15
Pause
# ————————————————————————–
# Initiate a Delta Sync with Azure AD Connect and set a timer of 5 minutes
# ————————————————————————–
Write-Host “Synchronizing Changes with Azure AD Connect.  Please allow 5 minutes for process to complete.  You will be prompted when to continue.” -ForegroundColor Green
Start-AdSyncSyncCycle -PolicyType Delta
Write-Host “PLEASE BE PATIENT – Confirm the Distribution Lists have been removed from Office 365 Azure AD before continuing” -ForegroundColor Green
sleep 300
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
Pause
# ————————————————————————–
# Complete the process by renaming the Cloud copies to the original names
# ————————————————————————–
Write-Host “Updating the placeholder Distribution Lists to replace the original AD synchronized Distribution Lists” -ForegroundColor Green
If (test-path $ExportDirectory) {
    $Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select -expandproperty Identity
    foreach ($group in $identities) {
        $TempDG = Get-DistributionGroup “Cloud-$Group”
        $TempPrimarySmtpAddress = $TempDG.PrimarySmtpAddress
        [System.IO.Path]::GetInvalidFileNameChars() | ForEach {$Group = $Group.Replace($_,’_’)}
        $OldAddresses = @(Import-Csv “$ExportDirectory\$Group.csv”)
        $NewAddresses = $OldAddresses | ForEach {$_.EmailAddress.Replace(“X500″,”x500”)}
        $NewDGName = $TempDG.Name.Replace(“Cloud-“,””)
        $NewDGDisplayName = $TempDG.DisplayName.Replace(“Cloud-“,””)
        $NewDGAlias = $TempDG.Alias.Replace(“Cloud-“,””)
        $NewPrimarySmtpAddress = ($NewAddresses | Where {$_ -clike “SMTP:*”}).Replace(“SMTP:”,””)
    Write-Host “Converting Cloud-$Group to $Group”
        Set-DistributionGroup `
            -Identity $TempDG.Name `
            -Name $NewDGName `
            -Alias $NewDGAlias `
            -DisplayName $NewDGDisplayName `
            -PrimarySmtpAddress $NewPrimarySmtpAddress `
            -HiddenFromAddressListsEnabled $False `
            -BypassSecurityGroupManagerCheck
        Set-DistributionGroup `
            -Identity $NewDGName `
            -EmailAddresses @{Add=$NewAddresses} `
            -BypassSecurityGroupManagerCheck
        Set-DistributionGroup `
            -Identity $NewDGName `
            -EmailAddresses @{Remove=$TempPrimarySmtpAddress} `
            -BypassSecurityGroupManagerCheck
    sleep 3
    }
    }
Write-Host “Completed” -ForegroundColor Green
Write-Host “———————————————————————————————-” -ForegroundColor Cyan
# Re-Enable AD Sync Schedule
Set-ADSyncScheduler -SyncCycleEnabled $true
Write-Host “The conversion process happens in Exchange and can take a while to reflect in Azure AD”
Write-Host “Check to make sure that Azure AD is updated and now showing all of the Distribution Lists are converted to Cloud objects”
Pause