đ Executive Summary
TL;DR: Recreating Microsoft 365 Distribution Lists with PowerShell is complex due to various properties and nested members. This guide offers three methods: a quick CSV export, a PowerShell script generator for direct recreation commands, and a scalable Graph API/tooling approach for architectural solutions, helping overcome common migration challenges.
đŻ Key Takeaways
- A simple Get-DistributionGroup | Export-Csv is insufficient for true one-to-one recreation, as it misses critical properties like nested groups, member types, delivery management, and moderation settings.
- The âBuild-A-Scriptâ generator creates a runnable PowerShell script that exports commands to recreate Distribution Lists, including group creation, member addition, and settings like HiddenFromAddressListsEnabled and RequireSenderAuthenticationEnabled.
- For massive scale, CI/CD integration, or complex dependencies, the Microsoft Graph API offers a more robust, efficient, and programmatic approach than traditional Exchange cmdlets for M365 configuration management.
- Deciding between building custom PowerShell solutions (Solution 2) and buying dedicated migration tools (Solution 3) depends on project scale, repeatability, and budget, with Solution 2 often being the sweet spot for one-off tasks.
Struggling to export and recreate Microsoft 365 Distribution Lists with PowerShell? Senior DevOps Engineer Darian Vance shares three battle-tested methods, from quick scripts to robust architectural solutions, to master M365 group migrations.
From Panic to PowerShell: The Definitive Guide to Cloning M365 Distribution Lists
It was 2 AM on a Saturday. We were in the middle of a critical tenant-to-tenant migration for a major acquisition. The third-party migration toolâthe one that cost a small fortune and was promised to be âturnkeyââhad just thrown a sea of red errors and given up. Specifically, it failed to replicate hundreds of mission-critical Distribution Lists with their specific settings and nested members. The project manager was pacing, the coffee was getting cold, and the go-live window was shrinking. That night, we didnât have a fancy GUI to fall back on. We had PowerShell, grit, and the harsh reality that a simple âexportâ is never simple. Weâve all been there, staring at a blinking cursor, needing to solve a complex problem⌠right now.
The âWhyâ: Whatâs So Hard About a Simple List?
Before we dive into the scripts, letâs get one thing straight. A Microsoft 365 Distribution List (or Distribution Group) isnât just a simple list of email addresses. Itâs a full-fledged Exchange object with a ton of properties. A simple Get-DistributionGroup | Export-Csv will give you a flat file, but it misses the critical context needed for a true one-to-one recreation. You lose things like:
- Nested Groups: A DL that contains another DL as a member.
- Member Types: A list can contain Mailboxes, MailUsers, Contacts, and other Groups. You need to know which is which.
- Delivery Management: Who is allowed to send to this list? Is it restricted to internal users?
- Moderation Settings: Does a manager need to approve messages before theyâre sent?
- Hidden Status: Is the group hidden from the Global Address List (GAL)?
When you need to recreate a list, youâre not just populating members; youâre rebuilding a configuration object. Thatâs why the simple approach fails and why youâre probably here. Letâs get this sorted.
Solution 1: The âItâs 3 AM and This Just Has to Workâ CSV Method
Look, I get it. Sometimes you donât need a perfect, idempotent, infinitely scalable solution. You need a solution that works in the next 30 minutes. This is that solution. Itâs brute-force, itâs a bit clunky, but it gets the core informationâthe group details and the membersâout into a set of CSVs you can work with.
Step 1: Export Group Details and Members
This script grabs all your distribution groups and their members, then exports them into two separate CSV files. We keep them separate to make them easier to manage.
# --- Connect to Exchange Online first! ---
# Connect-ExchangeOnline -UserPrincipalName your-admin@yourdomain.com
# Define output paths
$groupInfoPath = "C:\temp\M365_DL_Info.csv"
$memberInfoPath = "C:\temp\M365_DL_Members.csv"
# Get all Distribution Groups and select key properties
Write-Host "Exporting Distribution Group properties..."
Get-DistributionGroup -ResultSize Unlimited | Select-Object DisplayName, PrimarySmtpAddress, Alias, HiddenFromAddressListsEnabled, RequireSenderAuthenticationEnabled | Export-Csv -Path $groupInfoPath -NoTypeInformation
# Create an array to hold all member data
$allMembers = @()
$allGroups = Get-DistributionGroup -ResultSize Unlimited
$progress = 0
foreach ($group in $allGroups) {
$progress++
Write-Host "Processing group $($progress) of $($allGroups.Count): $($group.DisplayName)"
$members = Get-DistributionGroupMember -Identity $group.PrimarySmtpAddress -ResultSize Unlimited
foreach ($member in $members) {
$memberInfo = [PSCustomObject]@{
GroupName = $group.DisplayName
GroupSmtpAddress = $group.PrimarySmtpAddress
MemberName = $member.DisplayName
MemberSmtpAddress = $member.PrimarySmtpAddress
RecipientType = $member.RecipientType
}
$allMembers += $memberInfo
}
}
Write-Host "Exporting all member information..."
$allMembers | Export-Csv -Path $memberInfoPath -NoTypeInformation
Write-Host "Export complete. Files are at $groupInfoPath and $memberInfoPath"
Darianâs Pro Tip: This method is a lifesaver for a quick audit or a simple migration. But notice whatâs missing? Delivery restrictions, moderation, and nested group hierarchies arenât cleanly captured. Itâs a âgood enoughâ solution for a crisis, not a long-term strategy. Youâll be doing a lot of manual work in the target tenant to recreate these from the CSVs.
Solution 2: The âBuild-A-Scriptâ Generator
This is where we move from being a sysadmin to a DevOps engineer. Why export data when you can export the commands to recreate the state? This approach generates a runnable PowerShell script that will recreate your distribution lists from scratch. Itâs repeatable, version-controllable, and far more reliable.
The Exporter Script (Run in the source tenant)
# --- Connect to Exchange Online first! ---
# Define the output script path
$recreationScriptPath = "C:\temp\Recreate_M365_DLs.ps1"
$commands = @()
# Add a header to the script
$commands += "# --- Generated Distribution List Recreation Script ---"
$commands += "# --- Generated on $(Get-Date) ---"
$commands += ""
$commands += "# Make sure you are connected to the TARGET Exchange Online tenant before running this."
$commands += ""
$allGroups = Get-DistributionGroup -ResultSize Unlimited
foreach ($group in $allGroups) {
Write-Host "Generating commands for: $($group.DisplayName)"
# Command to create the group
$commands += "#region Create Group: $($group.DisplayName)"
$commands += "Write-Host 'Creating group: $($group.DisplayName)'"
$commands += "New-DistributionGroup -Name `"$($group.DisplayName)`" -Alias `"$($group.Alias)`" -PrimarySmtpAddress `"$($group.PrimarySmtpAddress)`""
$commands += "" # Add a newline for readability
# Command to add members
$members = Get-DistributionGroupMember -Identity $group.PrimarySmtpAddress -ResultSize Unlimited
if ($members) {
$commands += "Write-Host '...Adding members to $($group.DisplayName)'"
foreach ($member in $members) {
# We use the member's primary SMTP address as the most reliable identifier
$commands += "Add-DistributionGroupMember -Identity `"$($group.PrimarySmtpAddress)`" -Member `"$($member.PrimarySmtpAddress)`" -Confirm:`$false"
}
}
# Command to set other important properties
$commands += "Write-Host '...Configuring settings for $($group.DisplayName)'"
$commands += "Set-DistributionGroup -Identity `"$($group.PrimarySmtpAddress)`" -HiddenFromAddressListsEnabled `$($group.HiddenFromAddressListsEnabled) -RequireSenderAuthenticationEnabled `$($group.RequireSenderAuthenticationEnabled)"
$commands += "#endregion"
$commands += ""
$commands += ""
}
Write-Host "Writing recreation script to $recreationScriptPath"
$commands | Out-File -FilePath $recreationScriptPath -Encoding utf8
Now you have a file, Recreate_M365_DLs.ps1, that you can take to your new tenant, review, and run. Itâs transparent, editable, and a perfect artifact to check into a Git repository for your migration project.
Solution 3: The âStop Hacking and Start Architectingâ Graph API & Tooling Approach
Both solutions above are great for one-off tasks. But what if this is part of a larger process? What if you manage hundreds of tenants? What if you need to do this every quarter? Constantly running manual PowerShell scripts from your workstation isnât a scalable architecture. Itâs time to think bigger.
When to Level Up:
-
Massive Scale: When youâre dealing with thousands of groups and tens of thousands of members, you need better performance and error handling than a simple
foreachloop. The Microsoft Graph API is built for this. Itâs faster, more efficient, and the modern way to interact with M365. - CI/CD Integration: If you want to manage your M365 configuration âas code,â you should be using a proper automation pipeline (like in Azure DevOps or GitHub Actions). Your scripts should be running from a service principal, not an adminâs interactive session. The Graph API is designed for this kind of programmatic, non-interactive access.
- Complex Dependencies: If your distribution lists have incredibly complex delivery restrictions (e.g., âonly accept messages from members of these 5 other groupsâ), scripting this with Exchange cmdlets becomes a nightmare of dependency management.
- You Have a Budget: Letâs be honest. Sometimes the best solution is to pay for a dedicated tool. Companies like Quest, BitTitan, and ShareGate have invested thousands of hours into solving these exact problems. Your time is valuable. A few hundred (or thousand) dollars for a tool might save you weeks of scripting and testing.
Comparing the Approaches
Hereâs how I decide which path to take:
| Approach | Pros | Cons |
| 1. The CSV Method | Fastest to write; good for quick audits. | Manual recreation; error-prone; misses key settings. |
| 2. The Script Generator | Repeatable; captures key settings; good for migrations. | Can be slow on large environments; doesnât handle all edge cases. |
| 3. Graph API / Tools | Most robust; scalable; auditable; handles all complexity. | Steep learning curve (Graph); can be expensive (Tools). |
My Final Take: For 90% of the one-off requests that land on my desk, I use Solution 2. Itâs the sweet spot between speed and reliability. But for any large-scale, repeatable, or architecturally significant project, we always start the conversation with Solution 3. Donât be a hero trying to script your way out of a problem that a dedicated tool has already solved. Know when to build and when to buy.
Hopefully, this gives you a clear path forward, whether youâre in the middle of a 2 AM fire drill or planning your next major project. Now go get some coffee.
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)