Revealing the True Members of Nested Distribution Groups in Exchange Online

Have you ever checked a distribution list in Outlook, only to see a handful of members, even though you know the entire company receives emails sent to that address? Welcome to the illusion of the Outlook Address Book.

When you try to uncover the actual recipients using the standard PowerShell cmdlet Get-DistributionGroupMember, you often hit a wall of red text: ManagementObjectNotFoundException.

Why does this happen? The standard cmdlet works perfectly fine for static distribution lists. However, as soon as your primary group contains nested Dynamic Distribution Groups or Microsoft 365 Groups (Unified Groups), the command fails. Furthermore, Outlook won't display these groups if they are hidden from the Global Address List (GAL) or because the local client cannot calculate dynamic membership rules on the fly.

To get the actual "routing truth" of your Exchange server, you need a script that dynamically detects the type of group and uses the correct cmdlet to unpack it.

The Solution: A Smart, Recursive PowerShell Script

This script takes a target distribution group and recursively resolves all nested members. It catches circular dependencies (Group A is in Group B, and Group B is in Group A), suppresses errors from orphaned objects, and handles Dynamic and M365 Groups correctly.

PowerShell

Function Get-NestedGroupMembers {
param (
[Parameter(Mandatory=$true)]
[string]$Identity,
[System.Collections.ArrayList]$ProcessedGroups = [System.Collections.ArrayList]::new()
) $Result = @() # Prevent circular nesting (infinite loops)
if ($ProcessedGroups.Contains($Identity)) { return $Result }
$ProcessedGroups.Add($Identity) | Out-Null # Retrieve the recipient to determine the exact object type
$GroupObj = Get-Recipient -Identity $Identity -ErrorAction SilentlyContinue if (-not $GroupObj) {
Write-Warning "Skipping '$Identity' - Object not found or invalid recipient."
return $Result
} $Members = @() # Use the appropriate cmdlet based on the group type
switch ($GroupObj.RecipientTypeDetails) {
"DynamicDistributionGroup" {
Write-Host "Reading Dynamic Group: $Identity" -ForegroundColor DarkYellow
$Members = Get-DynamicDistributionGroupMember -Identity $Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
}
"GroupMailbox" {
Write-Host "Reading Microsoft 365 Group: $Identity" -ForegroundColor DarkYellow
$Members = Get-UnifiedGroupLinks -Identity $Identity -LinkType Members -ErrorAction SilentlyContinue
}
"MailUniversalDistributionGroup" {
Write-Host "Reading Standard Distribution Group: $Identity" -ForegroundColor DarkYellow
$Members = Get-DistributionGroupMember -Identity $Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
}
"MailUniversalSecurityGroup" {
Write-Host "Reading Mail-Enabled Security Group: $Identity" -ForegroundColor DarkYellow
$Members = Get-DistributionGroupMember -Identity $Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
}
"MailNonUniversalGroup" {
Write-Host "Reading Local Distribution Group: $Identity" -ForegroundColor DarkYellow
$Members = Get-DistributionGroupMember -Identity $Identity -ResultSize Unlimited -ErrorAction SilentlyContinue
}
default {
# If it's not a group, it's a final recipient (User, Shared Mailbox, Contact, etc.)
$Result += $GroupObj
return $Result
}
} # Return if the group is empty or couldn't be read
if (-not $Members) { return $Result } foreach ($Member in $Members) {
# Secure a unique ID for the next recursive run (preferring PrimarySmtpAddress)
$MemberId = $Member.PrimarySmtpAddress
if (-not $MemberId) { $MemberId = $Member.Name } # Check if the member is another group
if ($Member.RecipientTypeDetails -match "Group" -or $Member.RecipientType -match "Group") {
$Result += Get-NestedGroupMembers -Identity $MemberId -ProcessedGroups $ProcessedGroups
}
else {
# Add actual recipient
$Result += $Member
}
} return $Result
} # ==========================================
# EXECUTION & OUTPUT
# ========================================== # Define your target group here
$TargetGroup = "hq_all@yourdomain.com" Write-Host "Starting recursive query for '$TargetGroup'..." -ForegroundColor Cyan # Call the function
$AllMembers = Get-NestedGroupMembers -Identity $TargetGroup # Remove duplicates (in case a user is part of multiple nested sub-groups)
$UniqueMembers = $AllMembers | Sort-Object PrimarySmtpAddress -Unique # Ensure accurate count
$MemberCount = @($UniqueMembers).Count # Output results to the console
$UniqueMembers | Sort-Object DisplayName | Format-Table DisplayName, PrimarySmtpAddress, RecipientTypeDetails -AutoSize Write-Host "========================================="
Write-Host "Total unique recipients: $MemberCount" -ForegroundColor Green
Write-Host "========================================="

How It Works

  1. Pre-Flight Check: Instead of blindly running Get-DistributionGroupMember, the script first uses Get-Recipient to inspect the object.
  2. Switch Logic: It routes the object to the correct command. M365 Groups get parsed by Get-UnifiedGroupLinks, while dynamic lists use Get-DynamicDistributionGroupMember.
  3. Error Suppression: By utilizing -ErrorAction SilentlyContinue, the script elegantly skips broken sync objects or orphaned entries without halting the entire process.
  4. Deduplication: A user might be in the Sales group and the Marketing group, both of which are nested in the All_Company group. The Sort-Object -Unique pipe ensures this user is only counted once, giving you an exact headcount of distinct mailboxes that will receive the email.

While Outlook relies on what is visible in the client Address Book, this script queries the actual Exchange routing architecture. If you ever need to verify exact mail delivery metrics for compliance or large-scale internal communications, this PowerShell approach is the only way to get the real numbers.

Back to Homepage