How To Move Distribution Lists to Exchange Online

move distribution lists

I’ve been thinking about writing this script to move Distribution Lists to Exchange Online for a while now. That is because there is no native method that I ever found to migrate these DLs to Exchange Online when you are moving away from an On-Premises Exchange Server. Traditional Distribution Lists are essentially mail-enabled AD groups, so they aren’t tied to an object that can be moved to Exchange Online directly. So that essentially leads to one of two scenarios:

  1. You leave the Distribution lists on-prem and maintain an on-prem Exchange Server (or use the Exchange PowerShell offering to maintain your DLs)
  2. You delete and recreate all Distribution Lists that exist in the environment.

Neither of those are very appealing, especially the first one, given the number of vulnerabilities that Exchange Server has had recently. For me, that meant option number 2 is the best option. However, since that task is both tedious and prone to error, we have to find a way to do it both quickly, and reliably. That is what we are going to talk about in this post.

Unlike maybe some other posts I have done, to make this truly custom in your instance, you need to have some basic PowerShell knowledge to be able to customize the different cmdlets to meet your specific needs.

DISCLAIMER

Please understand that the content herein is for informational purposes only. This existence and contents shall not create an obligation, liability or suggest a consultancy relationship. In further, such shall be considered as is without any express or implied warranties including, but not limited to express and implied warranties of merchantability, fitness for a particular purpose and non-infringement. There is no commitment about the content within the services that the specific functions of the services or its reliability, applicability or ability to meet your needs, whether unique or standard. Please be sure to test this process fully before deploying in ANY production capacity, and ensure you understand that you are doing so at your own risk.

Table of Contents

Notes About the Process
The “Backup” Script
The “Recreate” Script
Ideas for Expansion
Conclusion


Notes About the Process

I am using Exchange Online PowerShell V3 while running this. In theory, most versions should work since the distribution list cmdlets go back to Exchange Server 2013 but consider this if you are having issues. Also, you may need to change the “Connect-ExchangeOnline” line if you need a different method of getting connected.

A few things about this process:

First and foremost, both of these scripts are relatively simple in nature. That is what I needed for my environment. So please understand, that I DO NOT thing this will work perfectly, 100% of the time, for everyone. Consider what I have created as a starting point for your own process. I’ll explain my logic and reasoning as I go, but I just want to be clear that there is plenty of room for expansion with these scripts. I will post a few ideas and things that I hadn’t fully worked into the process yet but may add later.

Second, please, please, please test this before you do anything live. Unless you have a reliable process that you use to backup all your On-Prem distribution lists (Veeam Active Directory Backup for example), please use test DLs to perfect this. Even though the first script will back up everything and all the settings, it is much better to have a backup tool that can restore something rather than recreate it. I will be demonstrating this in my Free Azure instance, which would also be a decent place to test this for yourself.

Finally, I am not a PowerShell expert. I am open to feedback and ideas and will gladly welcome any suggestions you have. You can get me on Twitter most easily, and I try to check it nearly every day. Alternatively, these will be on my Github, so you can just submit a Pull request, or an Issue and I will respond there.

Step-By-Step of the Overall Process

  1. Export all qualifying Distribution List settings and members via the “Backup” script
  2. Delete On-Prem Distribution Lists
  3. Wait Until On-Prem Sync’s and directory synced DLs no longer exist in Exchange Online
  4. Run second script to recreate DLs in Exchange Online
  5. Confirm DLs match previous On-Prem versions

The “Backup” Script

You could absolutely call this an Export script as well. I just considered this as creating my “Oh Sh*t” fallback if it went terribly wrong. The advantage here, is that we don’t have to create an CSV files or lists to know what settings to configure for our Exchange Online versions of the distribution lists. Plus, because the script pulls them in based on specific criteria, it can pull in as many as match it, and you don’t have to manually create all those settings. If uses a simple method of scaling to try and minimize the time you are spending on the process.

You’ll notice that I have it scoped specifically to traditional distribution lists. I am skipping mail-enabled security groups because those may serve an on-prem purpose with NTFS permissions, or something application specific. Feel free to change that if you are not concerned about this.

The Script

#connect to Exchange Online
Connect-ExchangeOnline

#define the path where all exports will be saved
$path = "C:\temp\DLs"

#gather our information about our distribution lists
$onPremDLs = Get-DistributionGroup | Where-Object { ($_.IsDirSynced -EQ $true -and $_.GroupType -ne "Universal, SecurityEnabled")}

#Export a list of the names of all groups that will be effected
$onPremDLs | Select-Object Name | Export-Csv "$path\AllDL-List.csv"

#Loop to create the CSV backup files, each in their own directory as well as a list of members
foreach($op in $onPremDLs){
    $foldername = $op.Name
    $folder = "$path\$foldername"
    $file = $op.Name

    if (test-path $folder) {
    Write-host "Folder Exists"
    }else {
        mkdir $folder
    }
    #Full backup of all settings related to the DL
    Get-DistributionGroup $op | export-csv "$folder\$file-full.csv"

    #Partial backup pulling only the information that we want for our exchange online DLs
    Get-DistributionGroup $op | Select-Object Name,Alias,Description,DisplayName,Managedby,MemberJoinRestriction,
PrimarySmtpAddress,RequireSenderAuthenticationEnabled,GroupType | export-csv "$folder\$file.csv"
    
    #Get all group members and save to a file - we only need PrimarySmtpAddress
    Get-DistributionGroupMember $op | select-object PrimarySmtpAddress | Export-Csv "$folder\$file-members.csv"

}

The latest version of this script can always be found at my GitHub here.

The Break Down

I designed this so that each group was located in its own folder and had its information in its own CSV files. I had no intention of doing ALL distribution lists at the same time, so it made more sense for me to manage it so that I could pick and choose which I wanted to do each time. This script gives both a full and a partial CSV file of the settings for the DL.

I did this for 2 reasons:

  1. Since I was only doing a few DLs at a time, I had a few specific ones that needed some modifications. For example, I needed to change the Owner of a few DLs before they were moved. A few others I changed the primarySmtpAddress field because I wanted to standardize the naming convention after years of many admins having a hand in the design. Things like Office distribution lists and department lists come to mind here. The partial list CSVs made it much easier because I didn’t have to scroll through so many fields.
  2. I acknowledge that other folks might need different fields on their DLs than I did. I felt it was better to have both a partial and a full because regardless of which one you use, every field you need is there. There are some that cannot be modified during creation, but I will address that later.

I first started by running the script to get a good backup copy of every list that qualified. From there, I made a copy of each folder that was created so I could always have a known good copy to work from. Next, I removed any I wasn’t planning to move in the first batch. Once I was ready for part two, the base folder where everything was located looked something like this:

a preview before our move Distribution list script before it runs

From here, I added, removed or modified fields as needed. Now, the stage is set for step two in the process.


The “Recreate” Script

When it is finally time, (I recommend doing this during off-hours when these distribution lists aren’t expected to manage any email traffic. At this point, I have reviewed the CSVs I created with the first script and am ready to delete the on-prem versions of these distribution lists. If you are running in Hybrid, with Azure AD Connect, then keep in mind there will be a delay from when you delete the DLs and when they will disappear in Exchange Online.

Remember: Don’t run this until you have deleted the On-Prem distribution lists and they are no longer synced into Exchange Online. I didn’t include a check in this script though it could potentially be added without much issue.

Keep in mind, that you need very few actual fields to create a distribution list in Exchange Online. The point of this post is that you can accurately copy and paste distribution lists to Exchange Online. However, if something goes wrong or you misspelled something, keep in mind you should still be able to manage one-of instances in the GUI once it is recreated.

Here is a screenshot from my test environment before I run this next script:

before we have begun to move distribution lists to exchange online.

The Script

Connect-ExchangeOnline

#path to the back up files
$path = "C:\temp\DLs"
$names = (Get-ChildItem $path).Name

function New-GroupCreation {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [Boolean]$RequireSenderAuthenticationEnabled
    )

    #Define folder paths and gather files and data
    $folder = "$path\$na"
    $Properties = Import-csv "$folder\$na.csv"
    $mem = Import-csv "$folder\$na-members.csv"

    #Define variables based on previous group information
    $Name = $Properties.Name
    $Alias = $Properties.Alias
    $Description = $Properties.Description
    $DisplayName = $Properties.DisplayName
    $Managedby = $Properties.Managedby
    $MemberJoinRestriction = $Properties.MemberJoinRestriction
    $members = $mem.PrimarySmtpAddress
    $PrimarySmtpAddress = $Properties.PrimarySmtpAddress
    $RequireSenderAuthenticationEnabled  = $Properties.RequireSenderAuthenticationEnabled 
    $GroupType = "Distribution"

    #Create new DL based on given parameters
    New-DistributionGroup -Alias $alias -Description $Description -DisplayName $DisplayName -ManagedBy $Managedby -MemberJoinRestriction $MemberJoinRestriction -Members $members -Name $Name -PrimarySmtpAddress $PrimarySmtpAddress -RequireSenderAuthenticationEnabled $RequireSenderAuthenticationEnabled -Type $GroupType
}


#Dynamically create all groups based on the information that was pulled from the last backup
foreach($na in $names){
    New-GroupCreation
}

Disconnect-ExchangeOnline -Confirm:$false

The latest version of this script can always be found at my GitHub here.

The Break Down

If you notice it is written to dynamically use the information that exists in that folder. Meaning, you control the groups that are moved by the folders in that root folder. The main advantage (goal) of this is to allow you to use the original settings. In theory, you could run the first script, delete the distribution lists, change the second script to include the properties you need, and just run the second script. It should just work.

However, I don’t advise this. There may be legacy DLs that don’t get used any more. There may be settings that need changed and reviewed. The purpose of the scripts is to minimize the up-front work that needs done to recreate these groups in the cloud. Intentionality and planning are the MOST important parts of these types of migrations. Choose the settings you need and add them to the script. The entire list of settings can be found in Microsoft’s documentation here. I chose what I needed and what was practical.

Example After the Script is Run

the first move distribution list group was successful
Here we can see the script is successfully run and our groups are created.
We can confirm they exist in Exchange Online console now, and the sync status is cloud only
We can confirm they exist in Exchange Online console now, and the sync status is cloud only
I can see that there is 1 owner, and 5 members which matches the CSV I started with
I can see that there is 1 owner, and 5 members which matches the CSV I started with

Ideas for Expansion

Set-DistributionGroup

I thought this was an important section to include for a few reasons. First, there are settings that you may have configured in a distribution list, that aren’t available in the New-DistributionGroup cmdlet. When I was going thru this, I noticed that I couldn’t include the AcceptMessagesOnlyFromDLMembers parameter on the New-DistributionGroup cmdlet because it wasn’t available. I needed to include a Set-DistributionGroup line in there to make it work. It looked like this:

    $Group1 = ($Properties.AcceptMessagesOnlyFrom).Split()[0]
    $Group2 = ($Properties.AcceptMessagesOnlyFrom).Split()[1]
    Set-DistributionGroup -Identity $Name -AcceptMessagesOnlyFromDLMembers $Group1, $Group2

In this field in the CSV, there were two groups listed, with a single space between them. This property is a little temperamental (at least for me) in the syntax, and wouldn’t accept them in their given form, despite it being exactly how it was exported. I grabbed that field, confirmed I had two in each distribution list I was moving, and split it into two groups.

I was rushed for this part, so I didn’t fully get to have it search dynamically. This is one thing to consider. I will publish a more dynamic version of the recreate script eventually, but this was the v1.0 that I was able to get working. The docs for Set-DistributionGroup are located here.

Active Directory / Exchange Server Integration

I didn’t do it because I wanted more control over the process, but you could definitely include some full automation to remove the on-prem objects using PowerShell from Active Directory and/or Exchange Server. Then include a part to dynamically check and wait until it disappears from Exchange Online. It isn’t something I wanted to do, but it shouldn’t be too difficult to add for anyone familiar with Exchange PowerShell. This would allow for a fully automated process, but I still think the value will come from moving them in groups to make post-move support manageable.


Conclusion

Remember, just because the script worked with no errors, don’t assume it did what you wanted. It should do what you expected because you planned it out and know what it is doing. Things like this shouldn’t be rushed. At least not if you want to avoid issues with support after the fact. I personally strive for as little impact as possible. For me, that comes from proper planning.

I hope you found some value in this. I’d be curious to hear how it worked for you, or if you had issues. I love receiving feedback and look forward to hearing from you about this one. As always, hit me up on Twitter @SeeSmittyIT to let me know what you thought of this post. Thanks for reading!

Smitty

Curtis Smith works in IT with a primary focus on Mobile Device Management, M365 Apps, and Azure AD. He has certifications from CompTIA and Microsoft, and writes as a hobby.

View all posts by Smitty →