- Reads Full Access delegates from one AD group
- Reads Send As delegates from another AD group (can be same group if you want)
- Grants those permissions to one or more shared mailboxes in Exchange Online
- Uses proper error handling and
-WhatIfsupport
Assumptions
- You’re using Exchange Online with the ExchangeOnlineManagement module.
- Delegate groups are on-prem AD groups (synced to Entra ID), not purely cloud groups.
- Group members are users (not computers/contacts/etc.).
function Set-SharedMailboxDelegatesFromADGroups {
<#
.SYNOPSIS
Grants Full Access and Send As permissions on shared mailboxes based on AD group membership.
.DESCRIPTION
Reads members from one or two AD groups and applies:
- Full Access permissions from the Full Access AD group
- Send As permissions from the Send As AD group
For each shared mailbox provided, all members of the respective groups
are granted the appropriate rights in Exchange Online.
.PARAMETER SharedMailboxes
One or more shared mailbox identities (alias, primary SMTP address, or UPN).
.PARAMETER FullAccessGroup
The AD group whose user members will receive Full Access rights.
.PARAMETER SendAsGroup
The AD group whose user members will receive Send As rights.
This can be the same as FullAccessGroup if desired.
.PARAMETER Recursive
If specified, Get-ADGroupMember will be run with -Recursive to include nested group members.
.EXAMPLE
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP-SharedMailbox1-FullAccess" `
-SendAsGroup "GRP-SharedMailbox1-SendAs" `
-WhatIf
.EXAMPLE
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]","[email protected]" `
-FullAccessGroup "GRP-SharedMailbox-Delegates" `
-SendAsGroup "GRP-SharedMailbox-Delegates"
.NOTES
- Requires: ActiveDirectory module (on-prem / RSAT) and ExchangeOnlineManagement.
- Make sure you have already run Connect-ExchangeOnline.
- Always test with -WhatIf first in production environments.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[string[]]$SharedMailboxes,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$FullAccessGroup,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$SendAsGroup,
[switch]$Recursive
)
begin {
# Load AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
throw "Failed to import ActiveDirectory module. Install RSAT / AD tools first. Details: $($_.Exception.Message)"
}
Write-Verbose "Retrieving members from AD groups..."
# Helper to resolve AD group members to usable identities (UPN/mail/SamAccountName)
function Get-UserDelegatesFromGroup {
param(
[Parameter(Mandatory)]
[string]$GroupName,
[switch]$RecursiveLookup
)
try {
$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
}
catch {
throw "AD Group '$GroupName' not found. Details: $($_.Exception.Message)"
}
# Build parameters for Get-ADGroupMember
$gmParams = @{
Identity = $group.DistinguishedName
ErrorAction = 'Stop'
}
if ($RecursiveLookup) {
$gmParams.Recursive = $true
}
try {
$members = Get-ADGroupMember @gmParams | Where-Object {
$_.objectClass -eq 'user'
}
}
catch {
throw "Failed to get members for group '$GroupName'. Details: $($_.Exception.Message)"
}
if (-not $members) {
Write-Warning "Group '$GroupName' has no user members."
return @()
}
$delegateUsers = @()
foreach ($m in $members) {
try {
$adUser = Get-ADUser -Identity $m.DistinguishedName -Properties UserPrincipalName,mail,sAMAccountName -ErrorAction Stop
# Prefer UPN, then mail, then sAMAccountName
$identity = $adUser.UserPrincipalName
if (-not $identity) { $identity = $adUser.mail }
if (-not $identity) { $identity = $adUser.SamAccountName }
if ($identity) {
$delegateUsers += [PSCustomObject]@{
DisplayName = $adUser.Name
Identity = $identity
SamAccount = $adUser.SamAccountName
UPN = $adUser.UserPrincipalName
Mail = $adUser.mail
}
}
else {
Write-Warning "Could not determine a usable identity for '$($adUser.DistinguishedName)'. Skipping."
}
}
catch {
Write-Warning "Failed to resolve AD user '$($m.DistinguishedName)': $($_.Exception.Message)"
}
}
return $delegateUsers
}
# Resolve group members once (not per mailbox)
$script:fullAccessDelegates = Get-UserDelegatesFromGroup -GroupName $FullAccessGroup -RecursiveLookup:$Recursive
$script:sendAsDelegates = Get-UserDelegatesFromGroup -GroupName $SendAsGroup -RecursiveLookup:$Recursive
Write-Verbose ("Full Access delegates resolved: {0}" -f $script:fullAccessDelegates.Count)
Write-Verbose ("Send As delegates resolved: {0}" -f $script:sendAsDelegates.Count)
}
process {
foreach ($shared in $SharedMailboxes) {
Write-Host "Processing shared mailbox: $shared" -ForegroundColor Yellow
# --- Full Access ---
foreach ($delegate in $script:fullAccessDelegates) {
$target = $delegate.Identity
if ($PSCmdlet.ShouldProcess("Mailbox '$shared'", "Grant Full Access to '$target'")) {
try {
$fullParams = @{
Identity = $shared
User = $target
AccessRights = 'FullAccess'
InheritanceType = 'All'
AutoMapping = $true
ErrorAction = 'Stop'
}
Add-MailboxPermission @fullParams
Write-Host "Full Access granted: $target -> $shared" -ForegroundColor Green
}
catch {
Write-Host "Error granting Full Access ($target -> $shared): $($_.Exception.Message)" -ForegroundColor Red
}
}
}
# --- Send As ---
foreach ($delegate in $script:sendAsDelegates) {
$target = $delegate.Identity
if ($PSCmdlet.ShouldProcess("Mailbox '$shared'", "Grant Send As to '$target'")) {
try {
$sendAsParams = @{
Identity = $shared
Trustee = $target
AccessRights = 'SendAs'
Confirm = $false
ErrorAction = 'Stop'
}
Add-RecipientPermission @sendAsParams
Write-Host "Send As granted: $target -> $shared" -ForegroundColor Green
}
catch {
Write-Host "Error granting Send As ($target -> $shared): $($_.Exception.Message)" -ForegroundColor Red
}
}
}
}
}
}Code language: PHP (php)
Example Usage:
# 1. Connect to Exchange Online (once per session)
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName [email protected]
# 2. Run the function (paste it into your session or profile first)
# Scenario: One shared mailbox, separate AD groups for FA and SA
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP_BILLING_SHMBX_FULLACCESS" `
-SendAsGroup "GRP_BILLING_SHMBX_SENDAS" `
-Recursive `
-WhatIf # <- dry run first
# When output looks correct, run without -WhatIf:
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP_BILLING_SHMBX_FULLACCESS" `
-SendAsGroup "GRP_BILLING_SHMBX_SENDAS" `
-RecursiveCode language: PHP (php)
If you use the same AD group for both Full Access and Send As, just pass the same name for both parameters:
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP_SERVICE_SHMBX_DELEGATES" `
-SendAsGroup "GRP_SERVICE_SHMBX_DELEGATES" `
-Recursive
``
Code language: JavaScript (javascript)
Notes / Gotchas
-Recursivewill include nested groups; if you don’t want that, just omit it.- – The function only processes user objects from the AD group. If you store other objects there, they’re ignored.
- – It prefers
UserPrincipalName→mail→SamAccountNameto build the identity for Exchange. - You can absolutely extend this to also handle Send on Behalf if your workflows need that.
Short answer:
The scripts as written will append permissions; they will not remove existing delegates.
But they also don’t currently check for existing permissions, so you can end up with duplicate ACEs or “already has permission” style errors.
Let me break it down.
What Add-MailboxPermission does (Full Access)
This cmdlet adds a permission entry (ACE) to the mailbox’s ACL:
Add-MailboxPermission -Identity <shared> -User <user> -AccessRights FullAccess
- It does not wipe existing permissions.
- It will append another ACE for that user.
- In some cases, if an identical ACE already exists, you’ll see an error like:
The user already has 'FullAccess' permission on the mailbox.
- In other cases/environments, you can get multiple ACEs for the same user with the same rights (messy but functionally works).
So:
- Existing Full Access delegates stay as they are
- Group-based delegates from AD are added on top
- But our script doesn’t currently deduplicate them
What Add-RecipientPermission does (Send As)
Same behavior conceptually:
Add-RecipientPermission -Identity <shared> -Trustee <user> -AccessRights SendAs
- It adds a new permission entry.
- It does not remove or override existing Send As delegates.
- If the user already has Send As, you can see:
The trustee "X" already has SendAs permission to the recipient "Y".
Again:
- Existing Send As delegates are preserved
- Script adds more trustees based on the AD group
- No cleanup or pre-check in the current version
What the current AD-group-based script actually does
For each shared mailbox:
- It resolves members of:
$FullAccessGroup→ users to give Full Access$SendAsGroup→ users to give Send As
- For each user, it blindly calls:
Add-MailboxPermission … FullAccessAdd-RecipientPermission … SendAs
It never:
- Removes existing delegates
- Replaces existing permissions
- Checks if a delegate already has that permission
So effectively it is:
Append-only / additive.
You might just see some red “already has permission” errors on re-run, but it won’t strip anyone.
If you want it to be clean and idempotent
If you’d prefer:
- No duplicate ACEs
- No “already has” errors spam
- Ability to truly say “delegates for this mailbox are controlled by these groups”
Then we upgrade the logic to:
- Read current Full Access permissions on the shared mailbox
- Only add users who don’t already have Full Access
- Optionally: remove any Full Access users who are not in the AD group (if you want a strict “group is source of truth” model)
Same for Send As.
Example: Make Full Access additions idempotent
Inside the function, before the Full Access loop for each mailbox, we can pull current Full Access permissions:
$currentFullAccess = Get-MailboxPermission -Identity $shared |
Where-Object {
$_.AccessRights -contains 'FullAccess' -and
-not $_.IsInherited
}Code language: PHP (php)
Then for each $delegate:
$target = $delegate.Identity
$alreadyHasFA = $currentFullAccess | Where-Object {
$_.User.ToString() -ieq $target
}
if ($alreadyHasFA) {
Write-Host "Skipping Full Access for '$target' on '$shared' (already present)" -ForegroundColor Yellow
}
else {
# Add-MailboxPermission ...
}Code language: PHP (php)
We can do the same thing for Add-RecipientPermission using Get-RecipientPermission.
If you want replace behavior (controlled by groups)
If your goal is:
“These AD groups define who should have Full Access / Send As. Anyone else should be removed.”
We can:
- Get existing delegates
- Calculate:
- Who to add = in AD group but not currently delegate
- Who to remove = currently delegate but not in AD group
- Use:
Remove-MailboxPermissionfor Full AccessRemove-RecipientPermissionfor Send As
⚠️ That’s more destructive, so I’d absolutely wrap that in -WhatIf and probably log the changes.
Summary
- Current scripts are append-only.
- They do not overwrite or remove existing delegates.
- They simply add permissions based on AD group membership.
- They don’t currently check for existing permissions, so:
- You may see “already has permission” errors.
- Or you may get duplicate permission entries.