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.
What you need
Modules and tools
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)
Options you can set
| Parameter | What it does |
|---|---|
-AdminUpn | Optional. The admin account to sign in with, for example admin@contoso.com. |
-OutputPath | Optional. Where to save the CSV. Default: MailboxPermissionReport_yyyyMMdd_HHmm.csv in the current folder. |
-RecipientTypeDetails | Optional. Mailbox types to include. Default: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox. |
-IncludeInherited | Optional switch. Also list inherited Full Access entries. By default only directly assigned permissions are listed. |
-SkipConnect | Optional switch. Use an Exchange Online session you already opened, and leave it open at the end. |
Step by step
- 1
Step 1
Install the ExchangeOnlineManagement module if needed (see connect to Exchange Online PowerShell).
- 2
Step 2
Save the script as
Get-CMMailboxPermissionReport.ps1, for example inC:\Scripts. - 3
Step 3
Open Windows PowerShell or PowerShell 7 from the Start menu and run
cd C:\Scripts. - 4
Step 4
Run
.\Get-CMMailboxPermissionReport.ps1 -AdminUpn admin@contoso.comand sign in when prompted. - 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.
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 }
}Commands documented here
Every command in the script is documented on these vendor pages. Test in a non-production tenant or environment first.