- Published on
How to Get Distribution Group Membership in Active Directory (PowerShell Script)
Checking a distribution list usually isn't about access control — it's about confirming who actually receives mail sent to it, which means the report needs email addresses front and center, not just names, and it needs to include contacts, which the standard cmdlet quietly drops.
The quick answer
Get-ADGroupMember -Identity "All-Staff" | Get-ADUser -Properties mail | Select-Object Name, mail
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Lists every member of a specific Active Directory distribution group,
with email addresses.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$GroupName,
[switch]$Recursive,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$group = Get-ADGroup -Identity $GroupName -Properties GroupCategory, member -ErrorAction Stop
if ($group.GroupCategory -ne 'Distribution') {
Write-Warning "$GroupName is a $($group.GroupCategory) group, not a distribution group — membership is still reported."
}
$memberParams = @{ Identity = $GroupName }
if ($Recursive) { $memberParams['Recursive'] = $true }
$members = Get-ADGroupMember @memberParams
$results = @(foreach ($member in $members) {
$mail = $null
if ($member.objectClass -in 'user', 'contact') {
$mail = (Get-ADObject -Identity $member.DistinguishedName -Properties mail -ErrorAction SilentlyContinue).mail
}
[PSCustomObject]@{
GroupName = $GroupName; MemberName = $member.Name; Email = $mail
ObjectClass = $member.objectClass; DistinguishedName = $member.DistinguishedName
}
})
# Get-ADGroupMember doesn't return contacts — pull them from the raw member
# attribute instead so a mail contact on the list isn't silently missing.
$contacts = $group.member |
Get-ADObject -Properties ObjectClass, mail -ErrorAction SilentlyContinue |
Where-Object { $_.ObjectClass -eq 'contact' }
foreach ($contact in $contacts) {
$results += [PSCustomObject]@{
GroupName = $GroupName; MemberName = $contact.Name; Email = $contact.mail
ObjectClass = 'contact'; DistinguishedName = $contact.DistinguishedName
}
}
if (-not $results) {
Write-Host "$GroupName has no members." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object MemberName
Write-Host "$GroupName has $(@($sorted).Count) member(s)." -ForegroundColor Cyan
$sorted | Format-Table MemberName, Email, ObjectClass -AutoSize
$noMailCount = @($sorted | Where-Object { $_.ObjectClass -eq 'user' -and -not $_.Email }).Count
if ($noMailCount -gt 0) {
Write-Host "$noMailCount member(s) are users with no email address set — they won't actually receive mail sent to this list." -ForegroundColor Yellow
}
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
-Recursive flattens nested distribution groups so every actual recipient shows up directly with their own email address, instead of just listing the nested group by name.
Contacts on distribution lists are common and easy to miss
External vendors, partner-company contacts, or anyone without an actual AD account often get added to a distribution list as a contact object rather than a user. Because Get-ADGroupMember doesn't return contacts, a naive membership check will just be wrong about who's actually on the list — this script cross-checks the group's raw member attribute specifically to catch that.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
- Multi-domain and forest coverage — Enumerate every domain in the forest in one pass and get one consolidated report instead of one per domain.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Get-ADDistributionGroupMembers.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.