Lucas Zuege's Blog

Export all users in a Distribution List with nested groups

A co-worker of mine asked if I was able to figure out a way to export all the users of a large distribution list we have out our company.

That list included multiple nested distribution lists within it, and the one-liner command we usually used, wasn't working.

What we used to use

The following is a one-liner we used to use...

Get-AdGroupMember -Identity 'GroupName' -Recursive | Get-AdUser -Properties * | Select Name,Mail | Export-csv -Path C:\Path\To\File.csv -Append -NoTypeInformation

Unfortunately, Get-ADGroupMember is limited to 5000 results (unless you change some server configurations, which we do not have access too.)

Since the Distribution Group we want to grab all the members from has well over the 5000 object limit, I needed to find a different way to do this.

My thought process

At first, I was thinking that I could use a foreach loop to run through each item in the Distribution Group. If the item was a user, it would run Get-ADUser, and if it was a nested group, it would run Get-ADGroup.

However, during a foreach loop, you are unable to modify an array variable, meaning that my idea of adding/removing members inside of the foreach loop would not work.

However, as you may know, I am currently learning Python, and the part of the book I am learning from that I am at is diving deep into while loops.

So, I had the idea of using a while loop.

On-Prem Script

$distList = 'GroupName'
$csvFile = 'C:\Path\To\File.csv'
[System.Collections.ArrayList]$members = (Get-ADGroup -identity $distList -Properties Member).Member
while ($members.count -gt 0) {
    try {
        Get-ADUser $members[0] -Properties * | Select DisplayName, mail | Export-CSV $csvFile -Append -NoTypeInformation
    } catch {
        try {
            $members += (Get-ADGroup -identity $members[0] -Properties Member).Member
        } catch {
            Get-ADObject $members[0] -Properties * | Select DisplayName, mail | Export-CSV $csvFile -Append -NoTypeInformation
        }
    }
    $members.count
    $members.Remove($members[0])
}

The above script is what was born from the idea of using a while loop.

Firstly, we call the $distList and $csvFile variables. After that, we create an "ArrayList" variable called $members. An ArrayList was needed because the while loop will add/remove items at will, and a normal Array is a fixed size. Trying to use remove on a fixed array results in an error.

After all the variables are called, the while loop begins. We want the loop to run until there are zero items left in the $members variable. To do that, we us the condition of $members.count -gt 0. In plain english, that is the count of the $members variable is greater than 0.

The while loop then runs the Get-ADUser command on the first item in the ArrayList, $members[0]. If it fails, it will run another command, the Get-ADGroup command. This is because, if the item isn't a user mailbox, it is likely a nested group. However, we don't want to just run Get-ADGroup, we want to add the members to the $members variable, so that we can export the user's information with the Get-ADUser command.

If Get-ADGroup also fails, then the member is likely a mailContact. If that is the case, we have Get-ADObject to pull the information on the mail contact.

Lastly, the script displays the current count, $members.count. This is used to see how many more items are left in the variable for the script to run. This is not needed, it was just a quick way to see progress.

Lastly, we remove the item we just used in the above commands, and then start the loop over from the next item.

After the loop runs through each item, and the last item in the ArrayList is removed, the while loop will end, which will also end the script. You can then check the .csv file in the location you entered for the $csvFile variable. This file should have every user that is a member of the group and all nested groups within it.

Better yet, the script will keep resolving nested groups. There is no limit to the number of nested groups.

Exchange Online-ifying the script

The above script was something I threw together in an hour, as it was an urgent request. It works, but it uses On-Prem Active Directory PowerShell commands.

With AzureAD PowerShell going away, I wanted to see if I could utilize the Exchange Online PowerShell module to accomplish the same results as the above scripts.

After some time, I ended up with the following...

# Ask user for the name of the Distribution/Mail-Enabled Security Group
$distList = Read-Host "Please enter the DisplayName or Email Address of the Distribution/Security Group"

# Check to see if the Exports folder is created. If not, create it.
$path = ".\Exports\Exchange"
if(-not (test-path $path)){
    New-Item -Path $path -ItemType Directory
}

# Get the current date for the CSV file name belowe.
$currentDate = Get-Date -Format "MM-dd-yyyy"
$csvFile = ".\Exports\Exchange\$distList-AllUsers-$currentDate.csv"

# Create an ArrayList variable with the Distribution Group's Members.
[System.Collections.ArrayList]$members = Get-DistributionGroupMember -identity $distList

$currentCount = 0
$totalMembersCount = 0

# Start the while loop to export user information.
while ($members.count -gt 0) {
    if ($members[0].RecipientType -eq 'UserMailbox') {
        try {
            Get-EXOMailbox -identity $members[0].PrimarySmtpAddress | Select DisplayName, PrimarySmtpAddress | Export-CSV $csvFile -Append -NoTypeInformation
        } catch {
            $user = $members[0].PrimarySmtpAddress
            $currentPath = (Get-Location).Path
            Write-Host "Unable to get the Mailbox information for $user. To see errors, Please check the log file -> $currentPath\Logs\Exchange\Get-AllUsersInDistList.log."
            $path = ".\Logs\Exchange"
            if(-not (test-path $path)){
                New-Item -Path $path -ItemType Directory
            }
            $currentTime = Get-Date -Format "MM/dd/yyyy hh:mmtt"
            Write-Output "$currentTime : $_" | Out-file .\Logs\Exchange\Get-AllUsersInDistList.log -Append
        }
    } elseif (($members[0].RecipientType -eq 'MailUniversalSecurityGroup') -or ($members[0] -eq 'MailUniversalDistributionGroup')) {
        try {
            $members += Get-DistributionGroupMember -identity $members[0].PrimarySmtpAddress -ResultSize Unlimited
        } catch {
            $group = $members[0].PrimarySmtpAddress
            $currentPath = (Get-Location).Path
            Write-Host "Unable to get the Group information for $group. To see errors, Please check the log file -> $currentPath\Logs\Exchange\Get-AllUsersInDistList.log."
            $path = ".\Logs\Exchange"
            if(-not (test-path $path)){
                New-Item -Path $path -ItemType Directory
            }
            $currentTime = Get-Date -Format "MM/dd/yyyy hh:mmtt"
            Write-Output "$currentTime : $_" | Out-file .\Logs\Exchange\Get-AllUsersInDistList.log -Append
        }
    } elseif ($members[0].RecipientType -eq 'MailContact') {
        try {
            $members[0] | Select DisplayName, PrimarySmtpAddress | Export-csv $csvFile -Append -NoTypeInformation
        } catch {
            $contact = $members[0].PrimarySmtpAddress
            $currentPath = (Get-Location).Path
            Write-Host "Unable to get the Contact information for $contact. To see errors, Please check the log file -> $currentPath\Logs\Exchange\Get-AllUsersInDistList.log."
            $path = ".\Logs\Exchange"
            if(-not (test-path $path)){
                New-Item -Path $path -ItemType Directory
            }
            $currentTime = Get-Date -Format "MM/dd/yyyy hh:mmtt"
            Write-Output "$currentTime : $_" | Out-file .\Logs\Exchange\Get-AllUsersInDistList.log -Append
        }
    } else {
        Write-Host "Not any of the above..." $members[0].PrimarySmtpAddress
    }
    $currentCount++
    $membersCount = $members.count

    if ($totalMembersCount -lt $members.count) {
        $totalMembersCount = $members.count
    }

    Write-Host "`rScript is currently running... Currently on user $currentCount out of $totalMembersCount total members." -NoNewLine
    $members.Remove($members[0])
}

As you can see, the Exchange Online variation of the script is a lot more detailed. This is because I ended up including it into the tools my co-workers utilize for Microsoft 365 management.

On top of the usual variable creations, the script utilizes the same while loop as the On-Prem version of the script, however, it includes error checking and a better way of showing how many more users are left, without spamming the PowerShell console with numbers.

The script will create an Export folder in whatever directory you are running the script from. It will also create a Log folder if any errors show up. You can change this within the script if you so feel!

#Guides #PowerShell #Programming #Windows