How To Create 'Dynamic' Exchange Distribution Lists Using Entra ID & Azure
Exchange Dynamic Distribution Lists are fantastic tools for ensuring that the correct users are receiving the correct email, however the attributes that can be used to filter these dynamic lists is somewhat limited, especially compared to Entra ID Dynamic Groups.
This post will show you how to utilize Entra ID Dynamic Groups, PowerShell and Azure Automation to create 'Dynamic' Distribution lists in Exchange Online.
Prerequisites
- User account with permissions to create a groups in Entra ID, and distribution lists in Exchange.
- Service Principal/Managed Identity with appropriate permissions to access Graph and Exchange Online via PowerShell (you will need to assign an Entra RBAC role to your SP/MI to enable Exchange management).
- Azure Automation account with Microsoft.Graph.Authentication, Microsoft.Graph.Groups and ExchangeOnlineManagement PowerShell 7.2 modules installed.
The first thing you're going to want to do is create a dynamic group in Entra ID that fits your criteria. In my initial use case, I needed to create 2 groups, one for all Exempt (Salaried) and all Non-Exempt (Hourly) staff. To retrieve this data I first had to extend the Entra ID Schema to allow this custom Active Directory attribute to be synced via Azure AD Connect (which is a whole separate post!).
- Go to entra.microsoft.com
- Identity > Groups > All Groups > New Group
- Give the group an appropriate name and under membership type select 'Dynamic User'.
- Select 'Add Dynamic Query' and configure your filtering. For this example I am doing a simple query that selects users who are enabled and have the employee id field populated -
(user.accountEnabled -eq true) and (user.employeeId -ne "")
- Hit Save and Create and your account will begin to populate with users who match the criteria.
- As the query populates the group, move into the Exchange Online admin center and go to Recipients > Groups > Add a group.
- Select 'Distribution', not 'Dynamic Distribution' and click next. Give the group an appropriate name and assign a group owner but do not add any members yet.
- Configure the group settings as desired and then select create group.
- Go to portal.azure.com now and navigate to your automation account. Once here, create an appropriately named new PowerShell 7.2 runbook.
- Before modifying the code, return to the Entra portal and note the object ID of the group you previously created. This can be found on the Overview page of the group. You will also need the email address of your distribution group.
- Add in the following code, which is broken down into sections below to explain each segment (full script at bottom) -
$ClientId = Get-AutomationVariable -Name 'Azure Automation ClientID'
$TenantId = Get-AutomationVariable -Name 'Azure Automation TenantID'
$Cert = Get-AutomationCertificate -Name '365 Automation'
try {
Connect-ExchangeOnline -CertificateThumbPrint $Cert.Thumbprint -AppID $ClientId -Organization "yourorg.onmicrosoft.com"
Write-Output "Connected to Exchange via Service Principal"
}
catch {
Write-Output "An error occurred:"
Throw $_
}
try {
Connect-MgGraph -ClientId $ClientId -TenantId $TenantId -CertificateThumbprint $Cert.Thumbprint -NoWelcome
Write-Output "Connected to Graph via Service Principal"
}
catch {
Write-Output "An error occurred:"
Throw $_
}
This section of the code retrieves the Client ID, Tenant ID and Service Principal certificate from the Automation Account secure store and saves to variables. Subsequently we connect to Graph and Exchange Online with these variables.
$entraGroupId = '1234a5b6-7892-0111-213c-14d1e5f16g1h'
$exchangeDistroEmail = 'mydistrolist@lightningsec.com'
#---------------------------------------------------#
# Get all members of Entra dynamic group and Exchange static distribution list
#---------------------------------------------------#
$allEntraGroupMembers = Get-MgGroupMember -GroupId $entraGroupId -All
$allExchangeDistroMembers = Get-DistributionGroupMember -Identity $exchangeDistroEmail -ResultSize Unlimited
#---------------------------------------------------#
# Compare the two objects, exchange is the reference object
#---------------------------------------------------#
$GroupDifferences = Compare-Object -ReferenceObject $allExchangeDistroMembers.ExternalDirectoryObjectId -DifferenceObject $allEntraGroupMembers.Id
Next we save the object ID and distribution list to variables, and use these variables to return all members of the Entra group and the distribution list. After these are returned, we use Compare-Object to find the differences between the two lists, using the user ID attribute as the unique identifier.
if ($GroupDifferences) {
Write-Output 'Updating Distribution List'
#---------------------------------------------------#
# Save the differences from each list to variables
#---------------------------------------------------#
$exchDifferences = $GroupDifferences | Where-Object {$_.SideIndicator -eq '<='}
$entraDifferences = $GroupDifferences | Where-Object {$_.SideIndicator -eq '=>'}
#---------------------------------------------------#
# Loop over the differences, removing entries in the
# Exchange list that aren't in Entra, and adding
# those in the Entra list that aren't in Exchange
#---------------------------------------------------#
foreach ($exchDifference in $exchDifferences) {
Write-Output "Removing $($exchDifference.InputObject) from Distribution List"
Remove-DistributionGroupMember -Identity $exchangeDistroEmail -Member $exchDifference.InputObject -Confirm:$false
}
foreach($entraDifference in $entraDifferences){
Write-Output "Adding $($entraDifference.InputObject) to Distribution List"
Add-DistributionGroupMember -Identity $exchangeDistroEmail -Member $entraDifference.InputObject
}
} else {
Write-Output 'No Changes to Distribution List'
}
If there are any differences returned by Compare-Object, we perform further processing. Using Where-Object and pipeline parameters we can determine which side of the comparison the difference originates from, and based on this we can use foreach loops to either remove the user from the exchange list, or add them to it.
- Hit Publish on your runbook, and select Schedules on the left menu bar. From here, add an appropriate schedule to your runbook, I have mine running every hour.
- On the first script run, it will fully populate your Exchange distribution list with the Entra ID group members, and on subsequent runs it will keep the membership in sync with Entra, making it a 'Dynamic' Exchange Distribution List. 😄
Full Script -
$ClientId = Get-AutomationVariable -Name 'Azure Automation ClientID'
$TenantId = Get-AutomationVariable -Name 'Azure Automation TenantID'
$Cert = Get-AutomationCertificate -Name '365 Automation'
try {
Connect-ExchangeOnline -CertificateThumbPrint $Cert.Thumbprint -AppID $ClientId -Organization "yourtenant.onmicrosoft.com"
Write-Output "Connected to Exchange via Service Principal"
}
catch {
Write-Output "An error occurred:"
Throw $_
}
try {
Connect-MgGraph -ClientId $ClientId -TenantId $TenantId -CertificateThumbprint $Cert.Thumbprint -NoWelcome
Write-Output "Connected to Graph via Service Principal"
}
catch {
Write-Output "An error occurred:"
Throw $_
}
$entraGroupId = '' # INSERT ENTRA GROUP ID HERE
$exchangeDistroEmail = '' # INSERT EXCHANGE DISTRO EMAIL HERE
#---------------------------------------------------#
# Get all members of Entra dynamic group and Exchange static distribution list
#---------------------------------------------------#
$allEntraGroupMembers = Get-MgGroupMember -GroupId $entraGroupId -All
$allExchangeDistroMembers = Get-DistributionGroupMember -Identity $exchangeDistroEmail -ResultSize Unlimited
#---------------------------------------------------#
# Compare the two objects, exchange is the reference object
#---------------------------------------------------#
$GroupDifferences = Compare-Object -ReferenceObject $allExchangeDistroMembers.ExternalDirectoryObjectId -DifferenceObject $allEntraGroupMembers.Id
if ($GroupDifferences) {
Write-Output 'Updating Distribution List'
#---------------------------------------------------#
# Save the differences from each list to variables
#---------------------------------------------------#
$exchDifferences = $GroupDifferences | Where-Object {$_.SideIndicator -eq '<='}
$entraDifferences = $GroupDifferences | Where-Object {$_.SideIndicator -eq '=>'}
#---------------------------------------------------#
# Loop over the differences, removing entries in the
# Exchange list that aren't in Entra, and adding
# those in the Entra list that aren't in Exchange
#---------------------------------------------------#
foreach ($exchDifference in $exchDifferences) {
Write-Output "Removing $($exchDifference.InputObject) from Distribution List"
Remove-DistributionGroupMember -Identity $exchangeDistroEmail -Member $exchDifference.InputObject -Confirm:$false
}
foreach($entraDifference in $entraDifferences){
Write-Output "Adding $($entraDifference.InputObject) to Distribution List"
Add-DistributionGroupMember -Identity $exchangeDistroEmail -Member $entraDifference.InputObject
}
} else {
Write-Output 'No Changes to Distribution List'
}