How To Move Distribution Lists to Exchange Online – Part 2

move distribution groups to exchange online part 2

Welcome back! We’re talking about how to move Distribution Lists to Exchange Online only to assist with the Exchange Server shutdown. If you missed part one, it is located here. I recommend checking that out first as there are many things about the original script and process that you should understand before moving forward here. This script expands upon the previous version to include a larger number of properties and settings. The goal is to make this as easy to move as possible.

When I initially started this post, it was going a completely different direction. So different in fact, that I deleted the original version because it was faster than fixing it. I realized that there was a lesson to be learned here, and that I should take this opportunity to talk about it. Let’s get started.


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

An Important Lesson
What’s New with Version 2.0
Planned Improvements

An Important Lesson

When I had started working on version 2.0 of this script, I followed a bunch of rabbit trails trying to make it work. The script kept growing and growing, without necessarily adding real value. It was working, but I was building it so specific to my own environment that it almost seemed a waste to keep working on it. The goal was to build a tool to help others, not just myself, so it felt like I had failed. However, I had forgotten an important lesson. There is a WHOLE COMMUNITY of people out there interested in helping you solve these problems.

I finally decided to post a piece of my problem on Reddit. The r/PowerShell community can be a great place to get advice and help with your code. So that is what I did. I submitted a post about a section I was having problems with, and after some back and forth, someone presented me with a solution that I didn’t even know existed: Export-Clixml.

Export-Clixml vs Export-CSV

Now if you have never used this (like me) then you may not understand why it is so helpful. Unlike Export-CSV, Export-Clixml saved your output in a way that will retain the objects and their properties. Export-CSV will save the data, but in a text/string format that can be read by an Import-CSV process and added as needed. This is worked for me in many case up to this point, but I had finally found a use case that didn’t work. It made my code overly complex and overbuilt to try and accomplish the same goal. Export-Clixml allowed me to export/import the information without losing those details that made it easier to call in a later Cmdlet.

The Lesson: Reach out to the Community

Don’t be afraid to ask for help. No one starts out as an expert but getting discouraged can cause you to quit long before you get there. Plus, it’s rare that you are trying to solve aa problem that no one else has seen before. If you are getting paid to solve problems, then the goal should be to find the solution. It doesn’t have to be a solution you came up with. Part of why I started this blog was because I had found so much useful information on the web, and I wanted to share and give back to the community.

Post your questions on Reddit or Twitter. Ask colleagues or friends (if they know something about what you need help with). Get involved with social groups or social media to meet like-minded peers that can help with these challenges. Special thanks to PMental on Reddit for the Export-Clixml tip, as it saved this from being a crazy long script.

What’s New with Version 2.0

If you remember from the first one, there were two scripts, one to pull the data, and one to apply it when recreating your Exchange Online Distribution lists. That is still the case since the changes are asynchronous and having a backup is a good way to protect against accidents.


The latest version of this script is below.
Get-DistributionGroupDetails.ps1 · SeeSmitty/Powershell (

For this script, the biggest change was to use Export-Clixml instead of Export-CSV. The reasons are listed above, but it provided a much better overall experience on the recreation side of things. I also removed the line about a partial backup file. I realized that I had used this for a specific purpose, and that it didn’t make a ton of sense being in the final script.

Additionally, I realized that Distribution lists, like regular mailboxes, may include a delegation component, and that needed to be accounted for. I included a check to look for delegation information when the groups were being backed up, and to gather than information when it was found. It will only create a separate file when delegation information is found, to speed up the process, and reduce the number of files created.

    #Get-delegation permissions for the distribution list in question
    $delegates = Get-RecipientPermission -Identity $op -ErrorAction SilentlyContinue
    if ([string]::IsNullOrEmpty($delegates)) {
        Write-Host "No Delegates for group: $op"
        $delegates | Export-Csv "$folder\$file-access.csv"
        Write-Host "Delegates Exist for group: $op"


For our second script, instead of a single function, we now have three. The latest version is below.
Set-DistributionGroupsBulk.ps1 · SeeSmitty/Powershell (


Our New-GroupCreation function includes the modified changes to support the XML change, albeit a tiny change. I removed the ManagedBy property and added the MemberDepartRestriction one instead. This is mostly because it is simple, and it makes sense to be included in the initial creation. ManagedBy was a little more complicated to add, so I moved it to the next section.


The Set-GroupProperties was an add to supplement the extras portion of part one. I realized that the simplicity of the original script made it less useful because it was too simple. Having the New-GroupCreation without all the properties made it almost more difficult. So that function includes everything else you can set in Set-DistributionGroup with the exception of properties that are exclusive to On-Prem Exchange, and the -RoomList switch which I couldn’t find a way to detect and indicate later. I will update the script if I find that.


Set-AccessRights incorporated the portion about delegation that I mentioned above. This will read and apply the delegated permissions that were previously applied. It’s a pretty simple function, so not a lot to mention there.

function Set-AccessRights {
    $path = "C:\temp\DLs"
    $folder = "$path\$na"

    #test to see if the file exists - apply the users if it does
if (Test-path "$folder\$na-access.csv") {
    $delegates = Import-csv "$folder\$na-access.csv"
    foreach($d in $delegates){
        #Assigns delegate access to each person who had it previously
        Add-RecipientPermission -Identity $na -Trustee $d.trustee -AccessRights $d.AccessRights -confirm:$false

    Write-Host "Delegate rights assigned to $Name successfully" -ForegroundColor DarkCyan

Comments and Output Notes

Finally, I added more comments about what was happening, and included console output so that you could see what was happening. I’m not sure that I will keep all of it, some of it might be excessive. We will see. This isn’t the final version of the script, so stay tuned to see what else comes out of it.

Planned Improvements

I already have some ideas about what I would like to change. Just looking at it, I can tell it isn’t optimized well yet, and could definitely benefit from some more efficient code. Unfortunately, I have to work on some other stuff for a while, so I have to come back to this project. I wanted to get something more useful than version 1.0 out there because that version was too simple to be really helpful. Here are some changes I’m looking for in my next version.


I appreciate scripts and functions that include logging. I’d like to get a little more details than a simple Start-Transcript, so I plan to look at different options for including some logging for troubleshooting and confirmation of settings application.


It isn’t slow per se, but again I believe it can be optimized. I’m going to investigate whether or not a hash table makes sense for something like this. I have a lot of duplicate code, and I suspect that either a hash table or 3-4 loops could potentially reduce the amount of code involved and speed up the process.

Group Justification/Usage

One thing I acknowledge that this script doesn’t take into account is whether or not every Distribution group needs moved. I would like to assume that everyone is performing regular access reviews and group usage, but I’m not 100% certain that is the case. I’m not sure exactly how I will implement this, but it’d be great if I could get some usage statistics included to help admins understand whether or not a group actually needs to move. Maybe it needs to go away and never come back instead :).


There you have it. A complete working version of the scripts that will help you migrate your Distribution Groups from On-Prem to Exchange Online only. Any step of the process that will make it easier to get away from Exchange Server on-prem is a win in my book. It is clear that both Microsoft and Hackers everywhere are pushing you to get rid of Exchange Server. Hopefully, this becomes on additional tool to make it easier to make that move and reduce your attack surface with your internet exposed on-prem resources.

As always, test everything and run this in small groups so you can confirm success. Don’t assume it works because there are no errors. 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!


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 →