Grant Shared Mailbox Permissions from AD Group Membership

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.

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 via Get-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 with AutoMapping = $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

The Connect-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 via Write-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)