Managing All Group Types with CRUD Operations
Introduction
Managing groups in Microsoft 365 is a routine task for
administrators and developers — but the challenge is that M365 exposes four
distinct group types, each managed through different Exchange Online cmdlets. A
script that works on a Unified Group will silently fail on a Distribution List.
In this post, we will build a single PowerShell script that
auto-detects the group type and exposes a clean CRUD interface — Read, Add,
Remove, and Modify — for both owners and members across all four group types:
•
Microsoft 365 Groups (Unified Groups)
•
Distribution Lists
•
Dynamic Distribution Lists
•
Mail-Enabled Security Groups
Prerequisites
No
Admin Rights Required The
ExchangeOnlineManagement module installs per-user with -Scope CurrentUser.
Before running the script, ensure you have:
•
PowerShell 5.1 or PowerShell 7+
•
An Exchange Online account with sufficient permissions
to manage groups
•
Internet connectivity (the script will install the
module automatically)
Understanding the Four Group Types
|
Group Type |
Use Case |
Owners
Stored In |
Member
Control |
|
Microsoft 365 Group |
Teams, SharePoint, shared mailbox |
UnifiedGroupLinks |
Manual |
|
Distribution List |
Email broadcast lists |
ManagedBy property |
Manual |
|
Mail-Enabled Security |
Email + permissions |
ManagedBy property |
Manual |
|
Dynamic Distribution List |
Auto-membership via OPATH filter |
ManagedBy property |
Automatic (filter) |
Note
on Dynamic DLs: Members
are computed from an OPATH filter and cannot be added or removed manually. The
script handles this gracefully with a clear warning.
CRUD Operations Overview
|
Operation |
$Operation
Value |
What It
Does |
|
Read |
Read |
Lists all owners and members with count |
|
Add Owner |
AddOwner |
Adds $TargetUPN as a group owner |
|
Remove Owner |
RemoveOwner |
Removes $TargetUPN; skips if only 1 owner |
|
Add Member |
AddMember |
Adds $TargetUPN as a group member |
|
Remove Member |
RemoveMember |
Removes $TargetUPN from members |
|
Modify |
Modify |
Updates DisplayName and/or Notes |
Script Configuration
At the top of the script, set these four variables before
running:
#
── Required ──────────────────────────────────────────────────────
$GroupEmail =
"group@yourdomain.com" #
Target group email address
$TargetUPN =
"user@yourdomain.com" #
User UPN for add/remove ops
$Operation =
"Read" #
Read | AddOwner | RemoveOwner
#
AddMember | RemoveMember | Modify
# ── For Modify only
───────────────────────────────────────────────
$NewDisplayName = "New Group Name" # Leave blank ("") to skip
$NewNotes = "Updated notes" # Leave blank ("") to skip
Full PowerShell Script
#
═══════════════════════════════════════════════════════════════════════════
# M365 All Group Types -
CRUD Operations Script
# Author : Sreekantha Reddy Udayagiri
# Blog : udayagirisreekanthreddy.com
# Supports: Microsoft 365
| Distribution List |
# Dynamic
Distribution List | Mail-Enabled Security
# Operations: Read | Add
Owner/Member | Remove Owner/Member | Modify Group
#
═══════════════════════════════════════════════════════════════════════════
# ── Inputs
────────────────────────────────────────────────────────────────
$GroupEmail =
"group@yourdomain.com" #
Group email address
$TargetUPN =
"user@yourdomain.com" #
UPN for add/remove operations
$Operation =
"Read" #
Read | AddOwner | RemoveOwner |
#
AddMember | RemoveMember | Modify
# ── For Modify operation only
─────────────────────────────────────────────
$NewDisplayName =
"" # Leave blank to skip
$NewNotes =
"" # Leave blank to skip
# ── Step 1: Install ExchangeOnlineManagement if not present
───────────────
$moduleName = "ExchangeOnlineManagement"
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
Write-Host
"Installing $moduleName ..." -ForegroundColor Yellow
Install-Module -Name
$moduleName -Scope CurrentUser -Force -AllowClobber
Write-Host
"$moduleName installed." -ForegroundColor Green
} else {
Write-Host
"$moduleName already present." -ForegroundColor Cyan
}
# ── Step 2: Connect
───────────────────────────────────────────────────────
Connect-ExchangeOnline -ShowBanner:`$false
# ── Step 3: Auto-detect group type
───────────────────────────────────────
$groupType = $null
$groupObject = $null
$groupObject = Get-UnifiedGroup -Identity $GroupEmail
-ErrorAction SilentlyContinue
if ($groupObject) { $groupType = "M365" }
if (-not $groupType) {
$groupObject =
Get-DistributionGroup -Identity $GroupEmail -ErrorAction SilentlyContinue
if ($groupObject) {
$groupType =
($groupObject.GroupType -match "SecurityEnabled") ?
"MailEnabledSecurity" : "DistributionList"
}
}
if (-not $groupType) {
$groupObject =
Get-DynamicDistributionGroup -Identity $GroupEmail -ErrorAction
SilentlyContinue
if ($groupObject) {
$groupType = "DynamicDistributionList" }
}
if (-not $groupType) {
Write-Warning
"Group not found: $GroupEmail"; Disconnect-ExchangeOnline
-Confirm:$false; exit 1
}
Write-Host "`nGroup
: $($groupObject.DisplayName)" -ForegroundColor Green
Write-Host "Type :
$groupType"
-ForegroundColor Green
# ── Helper: Get Owners
────────────────────────────────────────────────────
function Get-GroupOwners($email, $type, $obj) {
if ($type -eq
"M365") {
return
Get-UnifiedGroupLinks -Identity $email -LinkType Owners
}
return $obj.ManagedBy
| ForEach-Object {
Get-Recipient
-Identity $_ -ErrorAction SilentlyContinue
}
}
# ── Helper: Get Members
───────────────────────────────────────────────────
function Get-GroupMembers($email, $type) {
switch ($type) {
"M365"
{ return Get-UnifiedGroupLinks -Identity $email -LinkType Members }
"DistributionList"
{ return Get-DistributionGroupMember -Identity $email }
"MailEnabledSecurity"
{ return Get-DistributionGroupMember -Identity $email }
"DynamicDistributionList" {
Write-Warning
'Dynamic DLs use OPATH filters — members are computed dynamically.'
return
Get-DynamicDistributionGroupMember -Identity $email -ErrorAction
SilentlyContinue
}
}
}
# ══════════════════════════════════════════════════════════
# ── CRUD SWITCH ───────────────────────────────────────────
# ══════════════════════════════════════════════════════════
switch ($Operation) {
# ── READ
──────────────────────────────────────────────
"Read" {
Write-Host
"`n── OWNERS ──────────────────────────" -ForegroundColor Cyan
$owners =
Get-GroupOwners $GroupEmail $groupType $groupObject
$owners |
ForEach-Object {
Write-Host
" Owner :
$($_.PrimarySmtpAddress)" -ForegroundColor White
}
Write-Host
" Total owners: $(($owners |
Measure-Object).Count)" -ForegroundColor Yellow
Write-Host
"`n── MEMBERS ─────────────────────────" -ForegroundColor Cyan
$members =
Get-GroupMembers $GroupEmail $groupType
$members |
ForEach-Object {
Write-Host
" Member:
$($_.PrimarySmtpAddress)" -ForegroundColor White
}
Write-Host
" Total members: $(($members |
Measure-Object).Count)" -ForegroundColor Yellow
}
# ── ADD OWNER
─────────────────────────────────────────
"AddOwner" {
switch
($groupType) {
"M365" {
Add-UnifiedGroupLinks -Identity $GroupEmail -LinkType Owners -Links
$TargetUPN
}
{ $_ -in
"DistributionList","MailEnabledSecurity" } {
$existing
= Get-GroupOwners $GroupEmail $groupType $groupObject |
Select-Object -ExpandProperty DistinguishedName
$newOwner
= (Get-Recipient -Identity $TargetUPN).DistinguishedName
Set-DistributionGroup -Identity $GroupEmail -ManagedBy ($existing +
$newOwner) -BypassSecurityGroupManagerCheck
}
"DynamicDistributionList" {
$existing
= Get-GroupOwners $GroupEmail $groupType $groupObject |
Select-Object -ExpandProperty DistinguishedName
$newOwner
= (Get-Recipient -Identity $TargetUPN).DistinguishedName
Set-DynamicDistributionGroup -Identity $GroupEmail -ManagedBy ($existing
+ $newOwner)
}
}
Write-Host
"$TargetUPN added as owner." -ForegroundColor Green
}
# ── REMOVE OWNER
──────────────────────────────────────
"RemoveOwner" {
$owners =
Get-GroupOwners $GroupEmail $groupType $groupObject
if (($owners |
Measure-Object).Count -le 1) {
Write-Warning
"Only 1 owner exists. Removal skipped to avoid orphaned group."
break
}
$target = $owners
| Where-Object { $_.PrimarySmtpAddress -eq $TargetUPN }
if (-not $target)
{ Write-Warning "$TargetUPN is not an owner."; break }
switch
($groupType) {
"M365" {
Remove-UnifiedGroupLinks -Identity $GroupEmail -LinkType Owners -Links
$TargetUPN -Confirm:`$false
}
{ $_ -in
"DistributionList","MailEnabledSecurity" } {
$updated =
$owners | Where-Object { $_.PrimarySmtpAddress -ne $TargetUPN } |
Select-Object -ExpandProperty DistinguishedName
Set-DistributionGroup -Identity $GroupEmail -ManagedBy $updated
-BypassSecurityGroupManagerCheck
}
"DynamicDistributionList" {
$updated =
$owners | Where-Object { $_.PrimarySmtpAddress -ne $TargetUPN } |
Select-Object -ExpandProperty DistinguishedName
Set-DynamicDistributionGroup -Identity $GroupEmail -ManagedBy $updated
}
}
Write-Host
"$TargetUPN removed from owners." -ForegroundColor Green
}
# ── ADD MEMBER
────────────────────────────────────────
"AddMember"
{
switch
($groupType) {
"M365" {
Add-UnifiedGroupLinks -Identity $GroupEmail -LinkType Members -Links
$TargetUPN
}
{ $_ -in
"DistributionList","MailEnabledSecurity" } {
Add-DistributionGroupMember -Identity $GroupEmail -Member $TargetUPN
}
"DynamicDistributionList" {
Write-Warning 'Dynamic DLs use OPATH filters — members cannot be added
manually.'
}
}
Write-Host
"$TargetUPN added as member." -ForegroundColor Green
}
# ── REMOVE MEMBER
─────────────────────────────────────
"RemoveMember" {
switch
($groupType) {
"M365" {
Remove-UnifiedGroupLinks -Identity $GroupEmail -LinkType Members -Links
$TargetUPN -Confirm:`$false
}
{ $_ -in
"DistributionList","MailEnabledSecurity" } {
Remove-DistributionGroupMember -Identity $GroupEmail -Member $TargetUPN
-Confirm:`$false
}
"DynamicDistributionList" {
Write-Warning 'Dynamic DLs use OPATH filters — members cannot be removed
manually.'
}
}
Write-Host
"$TargetUPN removed from members." -ForegroundColor Green
}
# ── MODIFY GROUP
──────────────────────────────────────
"Modify" {
switch
($groupType) {
"M365" {
$params =
@{ Identity = $GroupEmail }
if
($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }
if
($NewNotes) {
$params["Notes"] = $NewNotes }
Set-UnifiedGroup @params
}
{ $_ -in
"DistributionList","MailEnabledSecurity" } {
$params =
@{ Identity = $GroupEmail }
if
($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }
if
($NewNotes) {
$params["Notes"] = $NewNotes }
Set-DistributionGroup @params
}
"DynamicDistributionList" {
$params =
@{ Identity = $GroupEmail }
if
($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }
if
($NewNotes) {
$params["Notes"] = $NewNotes }
Set-DynamicDistributionGroup @params
}
}
Write-Host
"Group updated successfully." -ForegroundColor Green
}
default {
Write-Warning
"Unknown operation: $Operation"
Write-Host
"Valid values: Read | AddOwner | RemoveOwner | AddMember | RemoveMember |
Modify"
}
}
# ── Disconnect ────────────────────────────────────────────
Disconnect-ExchangeOnline -Confirm:`$false
Write-Host
"`nDone." -ForegroundColor Green
How It Works — Step by Step
Step 1: Auto-Install the Module
The script checks for ExchangeOnlineManagement using
Get-Module -ListAvailable and installs it with -Scope CurrentUser if absent —
no admin rights needed.
Step 2: Auto-Detect Group Type
Instead of asking you to specify the group type, the script
probes in order: Unified Group → Distribution Group (then checks for
SecurityEnabled flag) → Dynamic Distribution Group. The first successful match
sets the $groupType variable used throughout.
Step 3: CRUD Switch Block
A switch statement routes execution based on $Operation.
Inside each operation, a nested switch handles the group-type-specific cmdlets
so the logic remains clean and readable.
Step 4: Owner Safety Guard
For RemoveOwner, the script first counts owners. If only one
owner exists, removal is skipped with a warning to prevent an orphaned group —
regardless of group type.
Step 5: ManagedBy Rebuild Pattern
For Distribution Lists, Mail-Enabled Security Groups, and
Dynamic DLs, owners are stored in the ManagedBy property. Adding or removing
requires rebuilding the full array and writing it back with
Set-DistributionGroup or Set-DynamicDistributionGroup.
Example Console Output
Read Operation
Group : Engineering Team
Type : M365
── OWNERS ──────────────────────────
Owner :
alice@contoso.com
Owner : bob@contoso.com
Total owners: 2
── MEMBERS ─────────────────────────
Member:
alice@contoso.com
Member: bob@contoso.com
Member:
carol@contoso.com
Total members: 3
RemoveOwner with 1 Owner Guard
Group : Finance DL
Type : DistributionList
WARNING:
Only 1 owner exists. Removal skipped to avoid orphaned group.
Dynamic DL Member Add Warning
Group : All Employees
Type :
DynamicDistributionList
WARNING:
Dynamic DLs use OPATH filters — members cannot be added manually.
CRUD Compatibility by Group Type
|
Operation |
M365 Group |
Distribution
List |
Mail-Enabled
Security |
Dynamic DL |
|
Read Owners |
✅ |
✅ |
✅ |
✅ |
|
Read Members |
✅ |
✅ |
✅ |
⚠️ Computed |
|
Add Owner |
✅ |
✅ |
✅ |
✅ |
|
Remove Owner |
✅ |
✅ |
✅ |
✅ |
|
Add Member |
✅ |
✅ |
✅ |
❌ Filter-based |
|
Remove Member |
✅ |
✅ |
✅ |
❌ Filter-based |
|
Modify |
✅ |
✅ |
✅ |
✅ |
Conclusion
This script gives you a single, reusable tool to manage all
four Exchange Online group types without switching cmdlets or remembering which
group uses which property. Key benefits:
•
Zero-touch module installation with -Scope CurrentUser
•
Auto group type detection — no manual configuration
•
Owner safety guard prevents orphaned groups
•
Graceful warnings for Dynamic DL filter-based
membership
•
Clean CRUD switch pattern — easy to extend