This script assigns Full Access and Send As permissions on shared mailboxes by reading membership from two AD groups. Run it when you want AD groups to be the source of truth for mailbox delegation instead of managing permissions one user at a time.
How It Works
Connect to Exchange Online
The script opens an Exchange Online session before the function is defined. That session needs to be active for any of the Exchange cmdlets to work.Resolve AD Group Members (once, in begin)
Both the Full Access and Send As AD groups are resolved a single time before any mailbox is touched. Each member is looked up individually to pull their UPN, mail attribute, and SAMAccountName. Identity preference order is UPN ā mail ā SAMAccountName. Members that can’t be resolved get a warning and are skipped. Using -Recursive expands nested groups.
Check Existing Permissions Before Acting
For each mailbox, the script pulls current non-inherited Full Access entries viaGet-MailboxPermission and current Send As entries via Get-RecipientPermission. These snapshots are used to skip anyone who already has the permission ā no duplicates, no unnecessary writes.
Grant Full Access
Add-MailboxPermission is called with AutoMapping = $true and InheritanceType = All. Only users not already in the current permission set get touched.
Grant Send As
Add-RecipientPermission handles Send As. Same skip logic applies. Confirm = $false suppresses the interactive prompt so this can run unattended.
WhatIf Support
The function is built with[CmdletBinding(SupportsShouldProcess)]. Pass -WhatIf to see exactly what would be granted without making any changes.
Usage
Requirements
- Exchange Online Management module (
Install-Module ExchangeOnlineManagement) - RSAT / ActiveDirectory PowerShell module installed on the machine running the script
- The account used to connect to Exchange Online needs Organization Management or equivalent rights to assign mailbox permissions
- Read access to the AD groups and their members
Running the Script
Update the call at the bottom of the script with your mailbox identity and group names, then run it:Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP-InternalTeam-Access" `
-SendAsGroup "GRP-InternalTeam-Access" `
-RecursiveCode language: PowerShell (powershell)
Dry Run First
Uncomment-WhatIf in the call or append it at the command line to preview changes without applying them:
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP-InternalTeam-Access" `
-SendAsGroup "GRP-InternalTeam-Access" `
-Recursive `
-WhatIfCode language: PowerShell (powershell)
Multiple Mailboxes
Pass an array or pipe values in:@("[email protected]", "[email protected]") | Set-SharedMailboxDelegatesFromADGroups `
-FullAccessGroup "GRP-Team-FA" `
-SendAsGroup "GRP-Team-SA"Code language: PowerShell (powershell)
Caveats
Permissions Are Never Removed
This script only adds. If someone is removed from the AD group, their mailbox permissions stay until you explicitly revoke them. You need a separate cleanup process if you want AD group membership to be a full source of truth.Duplicate-Check Is String Comparison
The existing-permission check compares the resolved identity string against$_.User or $_.Trustee using a case-insensitive match. If the same user has permissions recorded under a different format (e.g., DOMAINusername vs. UPN), the check may not catch it and the grant will be attempted again. Exchange will usually handle the duplicate gracefully, but expect noise in the output.
AutoMapping Is Always On
Full Access is granted withAutoMapping = $true. The mailbox will appear automatically in Outlook for every delegate. If your users don’t want that, you’ll need to change the parameter.
AD Group Must Contain Users Directly (or Use -Recursive)
If your AD groups nest other groups, members of those sub-groups are silently ignored unless you pass-Recursive. The filter explicitly drops any objectClass that isn’t user.
Users Without a UPN or Mail Attribute Are Skipped
If an AD user has no UPN, no mail attribute, and no SAMAccountName (unusual but possible in broken accounts), they get a warning and are skipped. No error is thrown, so check verbose/warning output to confirm everyone was processed.Exchange Online Session Must Already Be Open
TheConnect-ExchangeOnline call is at the top of the script, outside the function. If you dot-source or import the function elsewhere, that connection won’t happen automatically. Make sure you’re authenticated before calling the function.
No Logging to File
All output goes to the console viaWrite-Host and Write-Warning. If you’re running this as a scheduled task or need an audit trail, redirect output or add file logging yourself.
Full Script
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName [email protected]
function Set-SharedMailboxDelegatesFromADGroups {
<#
.SYNOPSIS
Grants Full Access and Send As permissions on shared mailboxes based on AD group membership.
.DESCRIPTION
- Reads members from one AD group for Full Access
- Reads members from one AD group for Send As
- For each shared mailbox:
- Adds Full Access for users in the FullAccessGroup (if they don't already have it)
- Adds Send As for users in the SendAsGroup (if they don't already have it)
- Does NOT remove existing delegates.
- Does NOT duplicate permissions.
.PARAMETER SharedMailboxes
One or more shared mailbox identities (alias, SMTP, or UPN).
.PARAMETER FullAccessGroup
AD group whose user members get Full Access.
.PARAMETER SendAsGroup
AD group whose user members get Send As (can be same as FullAccessGroup).
.PARAMETER Recursive
Include nested group members.
.EXAMPLE
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP-SharedMailbox1-FullAccess" `
-SendAsGroup "GRP-SharedMailbox1-SendAs" `
-Recursive `
-WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[string[]]$SharedMailboxes,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$FullAccessGroup,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$SendAsGroup,
[switch]$Recursive
)
begin {
# Import AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
throw "Failed to import ActiveDirectory module. Install RSAT/AD tools. Details: $($_.Exception.Message)"
}
Write-Verbose "Retrieving members from AD groups..."
# Helper: get users from AD group and convert to usable identities
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)"
}
$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 "No 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
# 1) Get existing Full Access & Send As permissions for this mailbox
$currentFullAccess = Get-MailboxPermission -Identity $shared |
Where-Object {
$_.AccessRights -contains 'FullAccess' -and
-not $_.IsInherited
}
$currentSendAs = Get-RecipientPermission -Identity $shared |
Where-Object {
$_.AccessRights -contains 'SendAs'
}
# 2) FULL ACCESS: only add if not already present
foreach ($delegate in $script:fullAccessDelegates) {
$target = $delegate.Identity
$alreadyHasFA = $currentFullAccess | Where-Object {
$_.User.ToString() -ieq $target
}
if ($alreadyHasFA) {
Write-Host "Skipping Full Access: '$target' already has Full Access on '$shared'" -ForegroundColor DarkYellow
continue
}
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
}
}
}
# 3) SEND AS: only add if not already present
foreach ($delegate in $script:sendAsDelegates) {
$target = $delegate.Identity
$alreadyHasSA = $currentSendAs | Where-Object {
$_.Trustee.ToString() -ieq $target
}
if ($alreadyHasSA) {
Write-Host "Skipping Send As: '$target' already has Send As on '$shared'" -ForegroundColor DarkYellow
continue
}
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
}
}
}
}
}
}
# *** CALL THE FUNCTION HERE ***
Set-SharedMailboxDelegatesFromADGroups `
-SharedMailboxes "[email protected]" `
-FullAccessGroup "GRP-InternalTeam-Access" `
-SendAsGroup "GRP-InternalTeam-Access" `
-Recursive `
#-WhatIfCode language: PowerShell (powershell)