|
| 1 | +# Mailbox Delegation Audit |
| 2 | + |
| 3 | +## Professional Summary |
| 4 | + |
| 5 | +This script connects to Exchange Online, enumerates all user and shared mailboxes, and produces a consolidated report of every delegate with: |
| 6 | + |
| 7 | +- **FullAccess** |
| 8 | +- **SendAs** |
| 9 | +- **SendOnBehalf** |
| 10 | + |
| 11 | +permissions per mailbox. The output is optimized for large tenants and can be exported to CSV or consumed by downstream governance and audit processes. |
| 12 | + |
| 13 | +## Why it matters |
| 14 | + |
| 15 | +In most organisations, mailbox delegation grows organically-shared mailboxes, executive assistants, team mailboxes, and legacy access that no one remembers granting. Over time, this creates risk: excessive access, stale permissions for leavers, and a lack of clear ownership. |
| 16 | + |
| 17 | +This script gives Microsoft 365 engineers a single, authoritative view of who can read from, and send as/on behalf of, any mailbox. It supports: |
| 18 | + |
| 19 | +- Access reviews for security and compliance |
| 20 | +- Offboarding checks to ensure leavers lose access |
| 21 | +- Audit evidence for internal and external regulators |
| 22 | +- Cleanup projects to remove unnecessary delegation |
| 23 | + |
| 24 | +## Benefits |
| 25 | + |
| 26 | +- Centralised visibility: One report covering all key mailbox delegate permissions. |
| 27 | +- Risk reduction: Quickly identify over-permissioned or unexpected delegates. |
| 28 | +- Operational efficiency: Avoid manual, per-mailbox checks in the EAC. |
| 29 | +- Audit-ready output: CSV-friendly structure for Power BI, Excel, or GRC tools. |
| 30 | +- Scales to large tenants: Uses efficient querying and avoids unnecessary processing. |
| 31 | + |
| 32 | +## Usage |
| 33 | + |
| 34 | +1. Register an Azure AD App |
| 35 | +2. Assign Exchange.ManageAsApp application permissions |
| 36 | +3. Upload a certificate and note: |
| 37 | + - Client ID |
| 38 | + - Tenant ID |
| 39 | + - Certificate Thumbprint |
| 40 | +4. Run the script from a secure automation host |
| 41 | +5. Review or export the $Report object |
| 42 | + |
| 43 | +# [PnP PowerShell](#tab/pnpps) |
| 44 | + |
| 45 | +```powershell |
| 46 | +
|
| 47 | +
|
| 48 | +
|
| 49 | +param( |
| 50 | + [Parameter(Mandatory)] |
| 51 | + [string]$TenantId, |
| 52 | +
|
| 53 | + [Parameter(Mandatory)] |
| 54 | + [string]$ClientId, |
| 55 | +
|
| 56 | + [Parameter(Mandatory)] |
| 57 | + [string]$CertificateThumbprint, |
| 58 | +
|
| 59 | + [string]$OutputPath = ".\MailboxDelegatesReport.csv", |
| 60 | +
|
| 61 | + [switch]$IncludeRoomAndEquipment |
| 62 | +) |
| 63 | +
|
| 64 | +# Connect using App-Only Certificate Authentication |
| 65 | +Connect-ExchangeOnline ` |
| 66 | + -AppId $ClientId ` |
| 67 | + -CertificateThumbprint $CertificateThumbprint ` |
| 68 | + -Organization $TenantId ` |
| 69 | + -ShowBanner:$false |
| 70 | +
|
| 71 | +# Determine mailbox types |
| 72 | +$recipientTypes = @("UserMailbox", "SharedMailbox") |
| 73 | +if ($IncludeRoomAndEquipment) { |
| 74 | + $recipientTypes += "RoomMailbox", "EquipmentMailbox" |
| 75 | +} |
| 76 | +
|
| 77 | +# Retrieve mailboxes |
| 78 | +$mailboxes = Get-ExoMailbox ` |
| 79 | + -RecipientTypeDetails $recipientTypes ` |
| 80 | + -ResultSize Unlimited ` |
| 81 | + -Properties DisplayName, PrimarySmtpAddress, GrantSendOnBehalfTo |
| 82 | +
|
| 83 | +# Pre-size collection for performance |
| 84 | +$Report = New-Object System.Collections.Generic.List[object] |
| 85 | +
|
| 86 | +foreach ($mailbox in $mailboxes) { |
| 87 | +
|
| 88 | + $identity = $mailbox.PrimarySmtpAddress.ToString() |
| 89 | +
|
| 90 | + # --- FullAccess --- |
| 91 | + $fullAccess = Get-ExoMailboxPermission -Identity $identity -ResultSize Unlimited | |
| 92 | + Where-Object { |
| 93 | + $_.AccessRights -contains "FullAccess" -and |
| 94 | + -not $_.IsInherited -and |
| 95 | + $_.User -ne "NT AUTHORITY\SELF" |
| 96 | + } |
| 97 | +
|
| 98 | + foreach ($perm in $fullAccess) { |
| 99 | + $Report.Add([pscustomobject]@{ |
| 100 | + MailboxIdentity = $identity |
| 101 | + MailboxDisplayName = $mailbox.DisplayName |
| 102 | + RecipientType = $mailbox.RecipientTypeDetails |
| 103 | + PermissionType = "FullAccess" |
| 104 | + DelegateIdentity = $perm.User |
| 105 | + IsInherited = $perm.IsInherited |
| 106 | + AccessRights = ($perm.AccessRights -join ",") |
| 107 | + }) |
| 108 | + } |
| 109 | +
|
| 110 | + # --- SendAs --- |
| 111 | + $sendAs = Get-ExoRecipientPermission -Identity $identity -ResultSize Unlimited | |
| 112 | + Where-Object { |
| 113 | + $_.AccessRights -contains "SendAs" -and |
| 114 | + -not $_.IsInherited -and |
| 115 | + $_.Trustee -ne "NT AUTHORITY\SELF" |
| 116 | + } |
| 117 | +
|
| 118 | + foreach ($perm in $sendAs) { |
| 119 | + $Report.Add([pscustomobject]@{ |
| 120 | + MailboxIdentity = $identity |
| 121 | + MailboxDisplayName = $mailbox.DisplayName |
| 122 | + RecipientType = $mailbox.RecipientTypeDetails |
| 123 | + PermissionType = "SendAs" |
| 124 | + DelegateIdentity = $perm.Trustee |
| 125 | + IsInherited = $perm.IsInherited |
| 126 | + AccessRights = ($perm.AccessRights -join ",") |
| 127 | + }) |
| 128 | + } |
| 129 | +
|
| 130 | + # --- SendOnBehalf --- |
| 131 | + if ($mailbox.GrantSendOnBehalfTo) { |
| 132 | + foreach ($delegate in $mailbox.GrantSendOnBehalfTo) { |
| 133 | +
|
| 134 | + $resolved = Get-ExoRecipient -Identity $delegate -ErrorAction SilentlyContinue |
| 135 | + $delegateIdentity = if ($resolved) { $resolved.PrimarySmtpAddress } else { $delegate.ToString() } |
| 136 | +
|
| 137 | + $Report.Add([pscustomobject]@{ |
| 138 | + MailboxIdentity = $identity |
| 139 | + MailboxDisplayName = $mailbox.DisplayName |
| 140 | + RecipientType = $mailbox.RecipientTypeDetails |
| 141 | + PermissionType = "SendOnBehalf" |
| 142 | + DelegateIdentity = $delegateIdentity |
| 143 | + IsInherited = $false |
| 144 | + AccessRights = "SendOnBehalf" |
| 145 | + }) |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +
|
| 150 | +# Export |
| 151 | +$Report | |
| 152 | + Sort-Object MailboxIdentity, PermissionType, DelegateIdentity | |
| 153 | + Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 |
| 154 | +
|
| 155 | +Write-Host "Mailbox delegate report generated: $OutputPath" -ForegroundColor Green |
| 156 | +
|
| 157 | +
|
| 158 | +``` |
| 159 | + |
| 160 | +## Output |
| 161 | + |
| 162 | +The script produces a structured dataset with: |
| 163 | + |
| 164 | +- MailboxIdentity |
| 165 | +- MailboxDisplayName |
| 166 | +- RecipientType |
| 167 | +- PermissionType |
| 168 | +- DelegateIdentity |
| 169 | +- IsInherited |
| 170 | +- AccessRights |
| 171 | + |
| 172 | +## Notes |
| 173 | + |
| 174 | +- Run periodically and store historical CSVs to track permission drift over time. |
| 175 | +- Consider feeding the CSV into Power BI for visual access review dashboards. |
| 176 | +- For very large tenants, you can parallelise mailbox processing with ForEach-Object -Parallel in PowerShell 7, if your operational standards allow it. |
| 177 | + |
| 178 | +## Contributors |
| 179 | + |
| 180 | +| Author(s) | |
| 181 | +|-----------| |
| 182 | +| [Josiah Opiyo](https://github.com/ojopiyo) | |
| 183 | + |
| 184 | +*Built with a focus on automation, governance, least privilege, and clean Microsoft 365 tenants-helping M365 admins gain visibility and reduce operational risk.* |
| 185 | + |
| 186 | +## Version history |
| 187 | + |
| 188 | +Version|Date|Comments |
| 189 | +-------|----|-------- |
| 190 | +1.0|Mar 01, 2026|Initial release |
| 191 | + |
| 192 | +[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)] |
| 193 | +<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/mailbox-delegation-audit" aria-hidden="true" /> |
| 194 | + |
0 commit comments