GitHub Icon Image
GitHub

Exchange Online Deleted & Recoverable Items Assessment

Summary

This PowerShell script connects to Exchange Online using certificate-based app-only authentication, enumerates all user mailboxes, and measures the storage consumed by:

  • Deleted Items
  • Recoverable Items
  • Combined Deleted/Recoverable content

The script ranks mailboxes by total deleted/recoverable storage consumption and exports the top 100 mailboxes to a timestamped CSV report. Processing activity and errors are recorded in a corresponding log file.

Why It Matters

Deleted and recoverable mailbox content can consume significant mailbox storage without being immediately visible during routine mailbox reviews.

This assessment provides M365 administrators with visibility into mailboxes containing unusually large volumes of deleted or recoverable data. It can support:

  • Mailbox storage investigations
  • Capacity and quota management
  • Retention and lifecycle reviews
  • Recoverable Items investigations
  • Mailbox cleanup assessments
  • Troubleshooting unexpected mailbox size growth
  • Operational or compliance-driven mailbox assessments

Benefits

  • Identifies high-consumption mailboxes by ranking them by total deleted/recoverable storage.
  • Separates Deleted Items from Recoverable Items, making the source of consumption easier to investigate.
  • Provides item counts alongside storage measurements.
  • Scales across the tenant by processing all user mailboxes.
  • Produces an auditable CSV report suitable for further analysis or operational reporting.
  • Provides execution logging to identify mailbox-level processing failures.
  • Uses app-only authentication, supporting unattended administrative execution.

Prerequisites

  • Exchange Online PowerShell module (ExchangeOnlineManagement)
  • Exchange Online application registration configured for certificate-based authentication
  • Valid certificate associated with the application
  • Appropriate Exchange Online application permissions and administrative configuration
  • Access to the certificate private key from the system executing the script

Configuration

Update the following variables before execution:

Variable Description
$ClientID Azure AD Application (Client) ID
$ThumbPrint Certificate thumbprint
$Tenant Microsoft 365 tenant name (e.g. contoso.onmicrosoft.com)
$TopN = 100 You can tweak this to your requirement
$OutputFolder Currently set to (C:\Temp\MailboxReport)

$TopN controls the number of highest-consuming mailboxes included in the final report.

  • Exchange Online PowerShell

# ============================================================
# CONFIGURATION
# ============================================================

$ClientID   = "xxxxxxxxxxxxxxxxxxxxx"
$ThumbPrint = "xxxxxxxxxxxxxxxxxxxxx"
$Tenant     = "contoso.onmicrosoft.com"

$TopN = 100

# ============================================================
# OUTPUT CONFIGURATION
# ============================================================

$OutputFolder = "C:\Temp\MailboxReport"

if (-not (Test-Path -LiteralPath $OutputFolder)) {
    New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
}

$TimeStamp = Get-Date -Format "yyyyMMdd_HHmmss"

$AssessmentFile = Join-Path `
    $OutputFolder `
    "LargestDeletedRecoverable_$TimeStamp.csv"

$LogFile = Join-Path `
    $OutputFolder `
    "LargestDeletedRecoverable_$TimeStamp.log"

# ============================================================
# LOGGING
# ============================================================

function Write-Log {
    param([string]$Message)

    $Line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message"

    Add-Content -Path $LogFile -Value $Line
    Write-Host $Line
}

# ============================================================
# CONNECT
# ============================================================

try {

    Import-Module ExchangeOnlineManagement -ErrorAction Stop

    Write-Log "Connecting to Exchange Online..."

    Connect-ExchangeOnline `
        -AppId $ClientID `
        -CertificateThumbprint $ThumbPrint `
        -Organization $Tenant `
        -ShowBanner:$false `
        -ErrorAction Stop

    Write-Log "Connected successfully."

    # ========================================================
    # GET USER MAILBOXES
    # ========================================================

    $Mailboxes = Get-EXOMailbox `
        -ResultSize Unlimited `
        -RecipientTypeDetails UserMailbox `
        -Properties DisplayName,UserPrincipalName,PrimarySmtpAddress `
        -ErrorAction Stop

    Write-Log "Found $(@($Mailboxes).Count) user mailboxes."

    # ========================================================
    # COLLECT DELETED / RECOVERABLE ITEMS
    # ========================================================

    $Results = foreach ($Mailbox in $Mailboxes) {

        try {

            # ------------------------------------------------
            # Deleted Items
            # ------------------------------------------------

            $DeletedFolders = Get-EXOMailboxFolderStatistics `
                -Identity $Mailbox.UserPrincipalName `
                -FolderScope DeletedItems `
                -ErrorAction Stop

            $DeletedBytes = 0
            $DeletedCount = 0

            foreach ($Folder in $DeletedFolders) {

                if ($Folder.FolderSize) {
                    $DeletedBytes += $Folder.FolderSize.Value.ToBytes()
                }

                if ($Folder.ItemsInFolder) {
                    $DeletedCount += $Folder.ItemsInFolder
                }
            }

            # ------------------------------------------------
            # Recoverable Items
            # ------------------------------------------------

            $RecoverableFolders = Get-EXOMailboxFolderStatistics `
                -Identity $Mailbox.UserPrincipalName `
                -FolderScope RecoverableItems `
                -ErrorAction Stop

            $RecoverableBytes = 0
            $RecoverableCount = 0

            foreach ($Folder in $RecoverableFolders) {

                if ($Folder.FolderSize) {
                    $RecoverableBytes += $Folder.FolderSize.Value.ToBytes()
                }

                if ($Folder.ItemsInFolder) {
                    $RecoverableCount += $Folder.ItemsInFolder
                }
            }

            # ------------------------------------------------
            # Combined
            # ------------------------------------------------

            $TotalBytes = `
                $DeletedBytes + $RecoverableBytes

            $TotalCount = `
                $DeletedCount + $RecoverableCount

            [PSCustomObject]@{
                DisplayName          = $Mailbox.DisplayName
                UserPrincipalName    = $Mailbox.UserPrincipalName
                PrimarySmtpAddress   = $Mailbox.PrimarySmtpAddress

                DeletedItemsGB      = [math]::Round(
                    $DeletedBytes / 1GB, 2
                )

                DeletedItemsCount   = $DeletedCount

                RecoverableItemsGB  = [math]::Round(
                    $RecoverableBytes / 1GB, 2
                )

                RecoverableItemsCount = $RecoverableCount

                TotalDeletedGB      = [math]::Round(
                    $TotalBytes / 1GB, 2
                )

                TotalDeletedCount   = $TotalCount
            }
        }
        catch {

            Write-Log `
                "WARNING: Failed to process $($Mailbox.UserPrincipalName): $($_.Exception.Message)"
        }
    }

    # ========================================================
    # TOP N
    # ========================================================

    $TopMailboxes = $Results |
        Sort-Object TotalDeletedGB -Descending |
        Select-Object -First $TopN

    $Rank = 1

    $TopMailboxes | ForEach-Object {

        $_ | Add-Member `
            -MemberType NoteProperty `
            -Name Rank `
            -Value $Rank

        $Rank++
    }

    # ========================================================
    # EXPORT
    # ========================================================

    $TopMailboxes |
        Select-Object `
            Rank,
            DisplayName,
            UserPrincipalName,
            PrimarySmtpAddress,
            DeletedItemsGB,
            DeletedItemsCount,
            RecoverableItemsGB,
            RecoverableItemsCount,
            TotalDeletedGB,
            TotalDeletedCount |
        Export-Csv `
            -Path $AssessmentFile `
            -NoTypeInformation `
            -Encoding UTF8

    # ========================================================
    # SUMMARY
    # ========================================================

    Write-Log "Assessment completed."
    Write-Log "Top $($TopMailboxes.Count) mailboxes exported."
    Write-Log "Report: $AssessmentFile"

    if ($TopMailboxes.Count -gt 0) {

        Write-Host ""
        Write-Host "Largest Deleted/Recoverable Items:"
        Write-Host ""
        Write-Host "$($TopMailboxes[0].DisplayName)"
        Write-Host "Total: $($TopMailboxes[0].TotalDeletedGB) GB"
        Write-Host "Deleted Items: $($TopMailboxes[0].DeletedItemsGB) GB"
        Write-Host "Recoverable Items: $($TopMailboxes[0].RecoverableItemsGB) GB"
        Write-Host ""
    }
}
catch {

    Write-Log "ERROR: $($_.Exception.Message)"
    throw
}
finally {

    Disconnect-ExchangeOnline `
        -Confirm:$false `
        -ErrorAction SilentlyContinue

    Write-Log "Disconnected from Exchange Online."
}


Output

Two timestamped files are generated in the configured output directory:

LargestDeletedRecoverable_yyyyMMdd_HHmmss.csv and LargestDeletedRecoverable_yyyyMMdd_HHmmss.log

CSV Report Fields

Field Description
Rank Mailbox ranking based on total deleted/recoverable storage
DisplayName Mailbox display name
UserPrincipalName Mailbox UPN
PrimarySmtpAddress Primary SMTP address
DeletedItemsGB Deleted Items storage in GB
DeletedItemsCount Number of Deleted Items
RecoverableItemsGB Recoverable Items storage in GB
RecoverableItemsCount Number of Recoverable Items
TotalDeletedGB Combined Deleted Items and Recoverable Items storage
TotalDeletedCount Combined item count

Logging & Error Handling

The script uses a dedicated logging function to record:

  • Connection status
  • Mailbox processing failures
  • Assessment completion
  • Report location
  • Top mailbox information
  • Fatal execution errors
  • Exchange Online disconnection

A failure processing an individual mailbox is logged as a warning and does not terminate the entire assessment.

Notes

  • The report is limited to user mailboxes through RecipientTypeDetails UserMailbox.
  • Folder statistics are queried individually for each mailbox, so execution time will increase with tenant size.
  • Storage values are calculated from Exchange Online folder statistics and converted to GB.
  • The script reports the top N mailboxes, rather than exporting every mailbox.
  • Credentials and certificate private keys should not be embedded in the script or stored in source control.
  • For production deployment, consider storing configuration separately and managing the certificate through an appropriate certificate store or automation platform.
  • The generated CSV contains mailbox identifiers and should be handled according to your organisation's data-handling requirements.

Contributors

Author(s)
Josiah Opiyo

Built with a focus on automation, governance, least privilege, and clean Microsoft 365 tenants - helping M365 admins gain visibility and reduce operational risk.

Version history

Version Date Comments
1.0 September 21, 2026 Initial release

Disclaimer

THESE SAMPLES ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

Back to top Script Samples
Generated by DocFX with Material UI