Set-Shared Mailbox Delegates From AD Groups

Assumptions Example Usage: If you use the same AD group for both Full Access and Send As, just pass the same name for both parameters: Notes / Gotchas Short answer:...

  • 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 -WhatIf support

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

  • -Recursive will 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 UserPrincipalNamemailSamAccountName to 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:

  1. It resolves members of:
    • $FullAccessGroup → users to give Full Access
    • $SendAsGroup → users to give Send As
  2. For each user, it blindly calls:
    • Add-MailboxPermission … FullAccess
    • Add-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:

  1. Read current Full Access permissions on the shared mailbox
  2. Only add users who don’t already have Full Access
  3. 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:

  1. Get existing delegates
  2. Calculate:
    • Who to add = in AD group but not currently delegate
    • Who to remove = currently delegate but not in AD group
  3. Use:
    • Remove-MailboxPermission for Full Access
    • Remove-RecipientPermission for 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.