~/2025/09/03/powershell-exchange-online-report-mailboxes-approaching-their-quota.md
PowerShell: Exchange Online – Report Mailboxes Approaching Their Quota
--- author: Tom Lasswell date: read: 5 min in: [ps, scripts] tags: [powershell, exchange-online, reporting] ---
$ grep -n '^#' post.md
Nobody notices a mailbox creeping up on its quota until it stops sending mail and a help desk ticket lands with "I can't send email" and no other context. Exchange Online does warn the user, but on the big plans the warning comes late: for Microsoft 365 E3/E5 and Business plans the defaults are a warning at 98 GB, prohibit send at 99 GB and prohibit send/receive at 100 GB, so the user gets one email with 1 GB of headroom. This script reports every mailbox above a percentage of its ProhibitSendQuota well before that, so you can nudge people, enable an archive, or fix a retention policy ahead of the bounce. It also reports the Recoverable Items folder, which has its own quota (30 GB, or 100 GB on hold) and fills up silently on mailboxes under litigation hold.
Requirements
- PowerShell 7, or Windows PowerShell 5.1.
- The ExchangeOnlineManagement module, version 3.0.0 or later (the REST-based EXO V3 module):
Install-Module -Name ExchangeOnlineManagement. - A connected session:
Connect-ExchangeOnlineinteractively, or app-only certificate authentication for a scheduled task. - A role that can read mailboxes and mailbox statistics tenant-wide, for example Global Reader or Exchange Administrator.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
WarningPercent | Int | No | Percentage of ProhibitSendQuota used that puts a mailbox in the report. Defaults to 80. |
RecipientTypeDetails | String | No | Mailbox types to check. Defaults to UserMailbox and SharedMailbox. |
OutputPath | String | No | CSV path to export the report to. If omitted, the report only prints to the console. |
Usage
Connect, then run with the default 80 percent threshold:
Connect-ExchangeOnline -ShowBanner:$false
.\Get-MailboxQuotaReport.ps1
Report anything over 90 percent, user mailboxes only, and save it for a scheduled task to email:
.\Get-MailboxQuotaReport.ps1 -WarningPercent 90 -RecipientTypeDetails UserMailbox -OutputPath 'C:\Reports\mailbox-quota.csv'
Unattended, with an app registration and a certificate in the machine store (see Microsoft's app-only authentication article for the one-time setup of the app, the Exchange.ManageAsApp permission and a directory role):
Connect-ExchangeOnline -CertificateThumbprint '<thumbprint>' -AppId '<application-id>' -Organization '<tenant>.onmicrosoft.com' -ShowBanner:$false
.\Get-MailboxQuotaReport.ps1 -OutputPath 'C:\Reports\mailbox-quota.csv'
Disconnect-ExchangeOnline -Confirm:$false
Sample console output, most urgent first:
Checked 1,184 mailbox(es); 4 at or above 80% of ProhibitSendQuota.
DisplayName Type UsedGB QuotaGB PercentUsed RecoverableGB RecoverablePercent ArchiveEnabled
----------- ---- ------ ------- ----------- ------------- ------------------ --------------
<display-name> UserMailbox 48.9 49.5 98.8 2.1 7.0 False
<display-name> UserMailbox 91.3 99.0 92.2 88.6 88.6 True
<display-name> UserMailbox 84.0 99.0 84.8 11.4 38.0 False
<display-name> UserMailbox 79.6 99.0 80.4 0.9 3.0 True
Script
<#
.SYNOPSIS
Reports Exchange Online mailboxes approaching their send quota.
.DESCRIPTION
Enumerates mailboxes with Get-EXOMailbox (Quota and Archive property sets), reads each mailbox's
TotalItemSize and TotalDeletedItemSize with Get-EXOMailboxStatistics, and returns the mailboxes
at or above a percentage of ProhibitSendQuota, with Recoverable Items usage against
RecoverableItemsQuota alongside. Sorted with the most urgent mailboxes first. Optionally exports
the report to CSV.
.PARAMETER WarningPercent
Percentage of ProhibitSendQuota used that puts a mailbox in the report. Defaults to 80.
.PARAMETER RecipientTypeDetails
Mailbox types to check. Defaults to UserMailbox and SharedMailbox.
.PARAMETER OutputPath
CSV path to export the report to. If omitted, the report only prints to the console.
.EXAMPLE
.\Get-MailboxQuotaReport.ps1 -WarningPercent 90 -OutputPath 'C:\Reports\mailbox-quota.csv'
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-09-03)
Requires: ExchangeOnlineManagement 3.0.0 or later, an active Connect-ExchangeOnline session
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[ValidateRange(1, 100)]
[int]$WarningPercent = 80,
[Parameter(Mandatory = $false)]
[string[]]$RecipientTypeDetails = @('UserMailbox', 'SharedMailbox'),
[Parameter(Mandatory = $false)]
[string]$OutputPath
)
function ConvertTo-ByteCount {
param ($Size)
# Quotas and sizes arrive either as a ByteQuantifiedSize (with ToBytes()) or as its
# string form, for example "4.92 GB (5,283,165,983 bytes)". "Unlimited" returns $null.
if ($null -eq $Size) {
return $null
}
if ($Size.PSObject.Properties['Value'] -and $Size.Value -and $Size.Value.PSObject.Methods['ToBytes']) {
return [double]$Size.Value.ToBytes()
}
$text = $Size.ToString()
if ($text -match '\(([\d,.\s]+) bytes\)') {
return [double]($Matches[1] -replace '[^\d]', '')
}
return $null
}
if (-not (Get-ConnectionInformation)) {
throw 'No Exchange Online session. Run Connect-ExchangeOnline first.'
}
Write-Verbose 'Retrieving mailboxes'
$mailboxes = Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $RecipientTypeDetails -PropertySets Quota, Archive
$checked = 0
$report = foreach ($mailbox in $mailboxes) {
$quotaBytes = ConvertTo-ByteCount -Size $mailbox.ProhibitSendQuota
if (-not $quotaBytes) {
# Unlimited quota: nothing to measure against.
continue
}
# Never pass an empty identity: a $null Identity returns statistics for every mailbox.
if ([string]::IsNullOrWhiteSpace($mailbox.UserPrincipalName)) {
continue
}
try {
$stats = Get-EXOMailboxStatistics -UserPrincipalName $mailbox.UserPrincipalName -ErrorAction Stop
} catch {
Write-Warning "Could not read statistics for $($mailbox.UserPrincipalName): $($_.Exception.Message)"
continue
}
$checked++
$usedBytes = ConvertTo-ByteCount -Size $stats.TotalItemSize
if ($null -eq $usedBytes) {
continue
}
$percentUsed = [math]::Round(($usedBytes / $quotaBytes) * 100, 1)
if ($percentUsed -lt $WarningPercent) {
continue
}
$deletedBytes = ConvertTo-ByteCount -Size $stats.TotalDeletedItemSize
$recoverableQuotaBytes = ConvertTo-ByteCount -Size $mailbox.RecoverableItemsQuota
$recoverablePercent = $null
if ($deletedBytes -and $recoverableQuotaBytes) {
$recoverablePercent = [math]::Round(($deletedBytes / $recoverableQuotaBytes) * 100, 1)
}
[PSCustomObject]@{
DisplayName = $mailbox.DisplayName
Mailbox = $mailbox.PrimarySmtpAddress
Type = $mailbox.RecipientTypeDetails
UsedGB = [math]::Round($usedBytes / 1GB, 1)
QuotaGB = [math]::Round($quotaBytes / 1GB, 1)
PercentUsed = $percentUsed
RecoverableGB = if ($deletedBytes) { [math]::Round($deletedBytes / 1GB, 1) } else { 0 }
RecoverablePercent = $recoverablePercent
ArchiveEnabled = $mailbox.ArchiveStatus -eq 'Active'
}
}
$sortedReport = @($report | Sort-Object -Property PercentUsed -Descending)
Write-Host ("Checked {0:N0} mailbox(es); {1} at or above {2}% of ProhibitSendQuota." -f $checked, $sortedReport.Count, $WarningPercent)
$sortedReport | Format-Table -Property DisplayName, Type, UsedGB, QuotaGB, PercentUsed, RecoverableGB, RecoverablePercent, ArchiveEnabled -AutoSize
if ($OutputPath) {
$sortedReport | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "Report exported to $OutputPath"
}
Notes
- Why the
Get-EXO*cmdlets. The original draft usedGet-MailboxandGet-MailboxStatistics. Microsoft's guidance for the V3 module is to use the REST-backedGet-EXO*cmdlets with property sets: without-PropertySetsor-Propertiesyou get only the Minimum set, and aGet-Mailboxcall returns at least 230 properties per mailbox whereGet-EXOMailboxreturns a handful. TheQuotaproperty set carriesProhibitSendQuota,IssueWarningQuota,ProhibitSendReceiveQuota,RecoverableItemsQuotaandUseDatabaseQuotaDefaults, and theMinimumset ofGet-EXOMailboxStatisticsalready includesTotalItemSizeandTotalDeletedItemSize, so no extra properties are requested for statistics. - Size parsing. The old draft called
.Value.ToBytes()directly onTotalItemSize. That works when the object is a liveByteQuantifiedSize, and fails when it arrives as its string form ("4.92 GB (5,283,165,983 bytes)"), which is what you often get over the REST module.ConvertTo-ByteCounthandles both and returns$nullforUnlimited. - The
$nullidentity trap. Microsoft's reference forGet-EXOMailboxStatisticswarns that$nullor a non-existent value for-Identityreturns every object, as if you had run the command with no identity at all. In a loop that turns one bad row into a full-tenant query, which is why the script skips blank UPNs. - Quotas come from licensing. In Exchange Online the quota values are set by the subscription and license assigned to the mailbox. You can lower them with
Set-Mailbox -ProhibitSendQuota, but you can't raise them past what the license allows. For example, Exchange Online Plan 1 and Office 365 E1 mailboxes prohibit send at 49.5 GB, while unlicensed shared mailboxes are limited to 50 GB until an Exchange Online Plan 2 license raises them to 100 GB. TotalItemSizedoesn't include Recoverable Items. Deleted items that are still recoverable count againstRecoverableItemsQuota(30 GB, or 100 GB when the mailbox is on hold), reported here asRecoverableGB. A mailbox on litigation hold can run out of Recoverable Items space while its main quota looks healthy, which is why that column is in the report.- Archives. Archive mailboxes aren't measured; add
-Archiveto a secondGet-EXOMailboxStatisticscall if you need them.ArchiveEnabledtells you whether moving mail to the archive with a retention policy is an option. Microsoft notes that retention policies only move mail to the archive automatically once the primary mailbox is larger than 10 MB, which isn't a constraint for anything this report flags. - On a tenant with thousands of mailboxes this is one statistics call per mailbox with a quota, so it takes a while. I run it as a nightly scheduled task rather than on demand.
Source
- About the Exchange Online PowerShell V3 module (properties and property sets)
- Property sets in Exchange Online PowerShell module cmdlets
- Get-EXOMailboxStatistics
- Get-EXOMailbox
- Set-Mailbox (ProhibitSendQuota)
- Exchange Online limits (capacity alerts, Recoverable Items quotas)
- App-only authentication for unattended scripts in Exchange Online PowerShell