Free script · PowerShell

Exchange Online mailbox permissions report

List who has Full Access, Send As and Send on Behalf on every Exchange Online mailbox in one CSV, so you can keep delegates together in the same migration batch and re-create permissions after a tenant move.

PowerShellRead-onlyUpdated 4 Nov 2026
Read-only: makes no changes
Before you run it

What you need

Admin roles

  • An Exchange Online admin role that can run Get-EXOMailbox, Get-EXOMailboxPermission and Get-EXORecipientPermission, for example Exchange Administrator

Runs on

  • Windows PowerShell 5.1 or PowerShell 7 on your admin PC (Windows or macOS)
Parameters

Options you can set

ParameterWhat it does
-AdminUpnOptional. The admin account to sign in with, for example admin@contoso.com.
-OutputPathOptional. Where to save the CSV. Default: MailboxPermissionReport_yyyyMMdd_HHmm.csv in the current folder.
-RecipientTypeDetailsOptional. Mailbox types to include. Default: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox.
-IncludeInheritedOptional switch. Also list inherited Full Access entries. By default only directly assigned permissions are listed.
-SkipConnectOptional switch. Use an Exchange Online session you already opened, and leave it open at the end.
How to run it

Step by step

  1. 1

    Step 1

    Install the ExchangeOnlineManagement module if needed (see connect to Exchange Online PowerShell).

  2. 2

    Step 2

    Save the script as Get-CMMailboxPermissionReport.ps1, for example in C:\Scripts.

  3. 3

    Step 3

    Open Windows PowerShell or PowerShell 7 from the Start menu and run cd C:\Scripts.

  4. 4

    Step 4

    Run .\Get-CMMailboxPermissionReport.ps1 -AdminUpn admin@contoso.com and sign in when prompted.

  5. 5

    Step 5

    Open the CSV the script names at the end. Filter the Permission column to see Full Access, Send As or Send on Behalf.

What you get: A UTF-8 CSV with one row per permission: Mailbox, MailboxUpn, MailboxType, Permission (FullAccess, SendAs or SendOnBehalf), Delegate, AccessRights, IsInherited and Error. The self entry (NT AUTHORITY\SELF) that every mailbox has is left out, as Microsoft describes it as the owner's own access.

The script

Get-CMMailboxPermissionReport.ps1

<#
.SYNOPSIS
    Get-CMMailboxPermissionReport.ps1

.DESCRIPTION
    Exports Full Access, Send As and Send on Behalf permissions for all Exchange Online
    mailboxes to one CSV. Read-only: it changes nothing in your tenant.

    Provided by cloud-migrations.au as is; test before use.
    Read the script before you run it and try it in a test tenant first.

.NOTES
    Requirements
      - ExchangeOnlineManagement module:  Install-Module -Name ExchangeOnlineManagement
      - An Exchange Online admin role that can run the cmdlets below
      - Windows PowerShell 5.1 or PowerShell 7
    Commands used (all documented by Microsoft):
      Connect-ExchangeOnline, Get-EXOMailbox (Delivery property set holds GrantSendOnBehalfTo),
      Get-EXOMailboxPermission (Full Access), Get-EXORecipientPermission (Send As),
      Disconnect-ExchangeOnline

.EXAMPLE
    .\Get-CMMailboxPermissionReport.ps1 -AdminUpn admin@contoso.com

.EXAMPLE
    .\Get-CMMailboxPermissionReport.ps1 -RecipientTypeDetails SharedMailbox -OutputPath C:\Reports\shared-perms.csv
#>
[CmdletBinding()]
param(
    [string]$AdminUpn,
    [string]$OutputPath = (Join-Path -Path (Get-Location) -ChildPath ('MailboxPermissionReport_{0}.csv' -f (Get-Date -Format 'yyyyMMdd_HHmm'))),
    [string[]]$RecipientTypeDetails = @('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox'),
    [switch]$IncludeInherited,
    [switch]$SkipConnect
)

$ErrorActionPreference = 'Stop'
$selfAccount = 'NT AUTHORITY\SELF'

function New-CMRow {
    param($Mailbox, $Permission, $Delegate, $AccessRights, $IsInherited, $ErrorText)
    [pscustomobject][ordered]@{
        Mailbox      = $Mailbox.PrimarySmtpAddress
        MailboxUpn   = $Mailbox.UserPrincipalName
        MailboxType  = $Mailbox.RecipientTypeDetails
        Permission   = $Permission
        Delegate     = $Delegate
        AccessRights = $AccessRights
        IsInherited  = $IsInherited
        Error        = $ErrorText
    }
}

# 1. Connect
if (-not $SkipConnect) {
    try {
        Write-Host 'Connecting to Exchange Online...'
        if ($AdminUpn) { Connect-ExchangeOnline -UserPrincipalName $AdminUpn }
        else { Connect-ExchangeOnline }
    }
    catch {
        Write-Error "Could not connect to Exchange Online: $($_.Exception.Message)"
        return
    }
}

# 2. Mailboxes, with the Delivery property set for GrantSendOnBehalfTo
try {
    Write-Host 'Reading mailboxes...'
    $mailboxes = @(Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $RecipientTypeDetails -PropertySets Minimum, Delivery)
}
catch {
    Write-Error "Get-EXOMailbox failed: $($_.Exception.Message)"
    if (-not $SkipConnect) { Disconnect-ExchangeOnline -Confirm:$false }
    return
}
Write-Host ("Found {0} mailboxes." -f $mailboxes.Count)

$results = New-Object System.Collections.Generic.List[object]
$i = 0
foreach ($mbx in $mailboxes) {
    $i++
    Write-Progress -Activity 'Reading mailbox permissions' -Status ("{0} of {1}: {2}" -f $i, $mailboxes.Count, $mbx.PrimarySmtpAddress) -PercentComplete (($i / [math]::Max($mailboxes.Count, 1)) * 100)

    # Full Access (and other mailbox-level rights)
    try {
        $perms = Get-EXOMailboxPermission -ExternalDirectoryObjectId $mbx.ExternalDirectoryObjectId
        foreach ($p in $perms) {
            if ($p.User -eq $selfAccount) { continue }
            if ($p.IsInherited -and -not $IncludeInherited) { continue }
            if ($p.Deny) { continue }
            $results.Add((New-CMRow $mbx 'FullAccess' $p.User ($p.AccessRights -join ',') $p.IsInherited ''))
        }
    }
    catch {
        $results.Add((New-CMRow $mbx 'FullAccess' '' '' '' $_.Exception.Message))
    }

    # Send As
    try {
        $sendAs = Get-EXORecipientPermission -ExternalDirectoryObjectId $mbx.ExternalDirectoryObjectId -AccessRights SendAs
        foreach ($s in $sendAs) {
            if ($s.Trustee -eq $selfAccount) { continue }
            $results.Add((New-CMRow $mbx 'SendAs' $s.Trustee ($s.AccessRights -join ',') '' ''))
        }
    }
    catch {
        $results.Add((New-CMRow $mbx 'SendAs' '' '' '' $_.Exception.Message))
    }

    # Send on Behalf (a mailbox property, not a permission entry)
    foreach ($d in @($mbx.GrantSendOnBehalfTo)) {
        if ($d) { $results.Add((New-CMRow $mbx 'SendOnBehalf' $d 'SendOnBehalf' '' '')) }
    }
}
Write-Progress -Activity 'Reading mailbox permissions' -Completed

# 3. Export
try {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Host ("Done. {0} permission rows written for {1} mailboxes." -f $results.Count, $mailboxes.Count)
    Write-Host "Report saved to: $((Resolve-Path -Path $OutputPath).Path)"
}
catch {
    Write-Error "Could not write the CSV to $OutputPath : $($_.Exception.Message)"
}
finally {
    if (-not $SkipConnect) { Disconnect-ExchangeOnline -Confirm:$false }
}
Sources

Commands documented here

Every command in the script is documented on these vendor pages. Test in a non-production tenant or environment first.