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 0If ($isconnected -ne “True”) {Connect-ExchangeOnline}##########################################################################################>clearWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-Host “!!!!!IMPORTANT!!!!!!” -ForeGroundColor RedWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-Host “!!!!!IMPORTANT!!!!!!” -ForeGroundColor RedWrite-Host “YOU MUST RUN THIS SCRIPT FROM THE DOMAIN CONTROLLER THAT IS RUNNING AZURE AD CONNECT” -ForeGroundColor Redsleep 5Write-Host “IF YOU ARE NOT PLEASE USE CTRL + C TO ESCAPE AND RUN FROM THE APPROPRIATE DOMAIN CONTROLLER” -ForeGroundColor RedWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-Host “It’s also important to note that this only affects Distribution Lists and not Mail-Enabled” -ForeGroundColor GreenWrite-Host “Security Groups. Mail-Enabled Security Groups must be handled differently.” -ForeGroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor Cyansleep 15PauseWrite-Host “Connecting to Exchange Online – installing all required PowerShell Modules and initiaing a connection” -ForegroundColor Green# ————————————————————————–# Load PowerShell Modules# ————————————————————————–Set-ExecutionPolicy RemoteSigned -ForceImport-Module ActiveDirectory[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12Install-Module -Name ExchangeOnlineManagement -ForceImport-Module ExchangeOnlineManagement#Connect & Login to ExchangeOnline (MFA)$getsession = get-pssession | select-object -Property State | select -expandproperty stateIf ($getsession -ne “Opened”) {Connect-ExchangeOnline}Write-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-HostWrite-hostWrite-Host “______________________________________________________________________________________________” -ForegroundColor CyanWrite-Host “Synchronized Distribution Groups with no ManagedBy settings will be defaulted to Organization” -ForeGroundColor YellowWrite-Host “Management. This value cannot be translated.” -ForeGroundColor YellowWrite-hostWrite-Host “You must set a default account value to replace Organization Management.” -ForeGroundColor GreenWrite-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 underwaySet-ADSyncScheduler -SyncCycleEnabled $falseWrite-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 bothWrite-Host ” Creating Directory: $WorkingDirectory”New-Item -ItemType directory -Path $WorkingDirectory | Out-NullWrite-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 existIf(!(Test-Path -Path $ExportDirectory )){Write-Host ” Creating Directory: $ExportDirectory”New-Item -ItemType directory -Path $ExportDirectory | Out-Null}}Write-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-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 stampif (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.extensionif ($extOnly.length -eq 0) {$nameOnly = $fileObj.Namerename-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”) -NoTypeInformationsleep 20}else {Write-Host “There are no appropriate Distribution Lists to migrate. Cancelling migration.”Break}Write-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-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,RecipientTypeIf ($Identities) {Foreach($group in $Identities) {$Members = Get-DistributionGroupMember $group.PrimarySmtpAddress -resultsize unlimitedif (@($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 stampif (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.extensionif ($extOnly.length -eq 0) {$nameOnly = $fileObj.Namerename-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 15Write-Host “Completed” -ForegroundColor GreenWrite-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 versionsIf ($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.Aliasif ((![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 GreenNew-DistributionGroup `-Name “Cloud-$OldName” `-Alias “Cloud-$OldAlias” `-DisplayName “Cloud-$OldDisplayName” `-ManagedBy $OldDG.ManagedBy `-Members $OldMembers `-PrimarySmtpAddress “Cloud-$OldPrimarySmtpAddress” | Out-NullSleep -Seconds 3Write-Host ” Setting Values For: Cloud-$OldDisplayName” -ForegroundColor GreenSet-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 `-BypassSecurityGroupManagerChecksleep 3}Else {Write-Host ” ERROR: The distribution group ‘$Group’ was not found” -ForegroundColor RedWrite-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 GreenWrite-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-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 BlackWrite-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 BlackWrite-host “WARNING – The Azure AZ Connect Sync Schedule is currently Suspended. You must complete the script or manually restart the Schedule.” -ForegroundColor Black -BackgroundColor RedWrite-Host “———————————————————————————————-” -ForegroundColor CyanWrite-host “Press Enter to delete the migrated Distribution Lists from Active Directory” -ForegroundColor CyanpauseIf (test-path ($WorkingDirectory + “DG_Details_Backup.csv”)) {$Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select -expandproperty Identityforeach ($group in $identities) {Remove-ADGroup -identity “$group” -confirm:$falsesleep 2}}Write-Host “All Distribution Lists have been removed from Active Directory” -ForegroundColor GreenWrite-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor Cyansleep 15Pause# ————————————————————————–# 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 GreenStart-AdSyncSyncCycle -PolicyType DeltaWrite-Host “PLEASE BE PATIENT – Confirm the Distribution Lists have been removed from Office 365 Azure AD before continuing” -ForegroundColor Greensleep 300Write-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor CyanPause# ————————————————————————–# 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 GreenIf (test-path $ExportDirectory) {$Identities = import-csv ($WorkingDirectory + “DG_Details_Backup.csv”) | select -expandproperty Identityforeach ($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 `-BypassSecurityGroupManagerCheckSet-DistributionGroup `-Identity $NewDGName `-EmailAddresses @{Add=$NewAddresses} `-BypassSecurityGroupManagerCheckSet-DistributionGroup `-Identity $NewDGName `-EmailAddresses @{Remove=$TempPrimarySmtpAddress} `-BypassSecurityGroupManagerChecksleep 3}}Write-Host “Completed” -ForegroundColor GreenWrite-Host “———————————————————————————————-” -ForegroundColor Cyan# Re-Enable AD Sync ScheduleSet-ADSyncScheduler -SyncCycleEnabled $trueWrite-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