Free script · PowerShell

Exchange Online mailbox size and archive report

Export every Exchange Online mailbox with its size, item count, archive size, last logon time and mailbox type to a CSV, so you can size migration batches and spot mailboxes that need cleaning up first.

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 and Get-EXOMailboxStatistics, 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. Leave it out to pick the account in the sign-in window.
-OutputPathOptional. Where to save the CSV. Default: MailboxSizeReport_yyyyMMdd_HHmm.csv in the current folder.
-RecipientTypeDetailsOptional. Mailbox types to include. Default: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox. Any value Microsoft lists for Get-EXOMailbox -RecipientTypeDetails works.
-SkipArchiveOptional switch. Do not query archive mailbox statistics (faster on large tenants).
-SkipConnectOptional switch. Use an Exchange Online session you already opened with Connect-ExchangeOnline, and leave it open at the end.
How to run it

Step by step

  1. 1

    Step 1

    Install the ExchangeOnlineManagement module if you have not already (see connect to Exchange Online PowerShell).

  2. 2

    Step 2

    Save the script as Get-CMMailboxSizeReport.ps1 in a working folder, for example C:\Scripts.

  3. 3

    Step 3

    Open Windows PowerShell or PowerShell 7 from the Start menu and change to that folder: cd C:\Scripts.

  4. 4

    Step 4

    Run .\Get-CMMailboxSizeReport.ps1 -AdminUpn admin@contoso.com and sign in when the Microsoft sign-in window appears.

  5. 5

    Step 5

    Wait for the progress bar to finish. The script prints the full path of the CSV at the end.

What you get: A UTF-8 CSV with one row per mailbox: DisplayName, UserPrincipalName, PrimarySmtpAddress, RecipientTypeDetails, ItemCount, TotalItemSize, TotalItemSizeBytes, DeletedItemCount, TotalDeletedItemSize, LastLogonTime, ArchiveEnabled, ArchiveItemCount, ArchiveTotalItemSize, ArchiveTotalItemSizeBytes and Error (blank unless that mailbox failed).

The script

Get-CMMailboxSizeReport.ps1

<#
.SYNOPSIS
    Get-CMMailboxSizeReport.ps1

.DESCRIPTION
    Exports Exchange Online mailbox sizes, item counts, archive size, last logon time
    and mailbox type to a CSV file. 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 Get-EXOMailbox and Get-EXOMailboxStatistics
      - Windows PowerShell 5.1 or PowerShell 7
    Commands used (all documented by Microsoft):
      Connect-ExchangeOnline, Get-EXOMailbox, Get-EXOMailboxStatistics, Disconnect-ExchangeOnline

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

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

$ErrorActionPreference = 'Stop'

# Sizes come back as text such as "1.2 GB (1,288,490,188 bytes)". Pull out the byte count for sorting in Excel.
function Get-CMBytes {
    param($Size)
    if ($null -eq $Size) { return $null }
    $m = [regex]::Match($Size.ToString(), '\(([\d,]+) bytes\)')
    if ($m.Success) { return [int64]($m.Groups[1].Value -replace ',', '') }
    return $null
}

# 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. Get the mailboxes. The Archive property set adds ArchiveGuid and ArchiveStatus.
try {
    Write-Host 'Reading mailboxes (this can take a while in large tenants)...'
    $mailboxes = @(Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $RecipientTypeDetails -PropertySets Minimum, Archive)
}
catch {
    Write-Error "Get-EXOMailbox failed: $($_.Exception.Message)"
    if (-not $SkipConnect) { Disconnect-ExchangeOnline -Confirm:$false }
    return
}
Write-Host ("Found {0} mailboxes." -f $mailboxes.Count)

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

    $row = [ordered]@{
        DisplayName               = $mbx.DisplayName
        UserPrincipalName         = $mbx.UserPrincipalName
        PrimarySmtpAddress        = $mbx.PrimarySmtpAddress
        RecipientTypeDetails      = $mbx.RecipientTypeDetails
        ItemCount                 = $null
        TotalItemSize             = $null
        TotalItemSizeBytes        = $null
        DeletedItemCount          = $null
        TotalDeletedItemSize      = $null
        LastLogonTime             = $null
        ArchiveEnabled            = ($null -ne $mbx.ArchiveGuid -and $mbx.ArchiveGuid -ne [guid]::Empty)
        ArchiveItemCount          = $null
        ArchiveTotalItemSize      = $null
        ArchiveTotalItemSizeBytes = $null
        Error                     = ''
    }

    try {
        # LastLogonTime is not in the Minimum property set, so ask for it with -Properties.
        $stats = Get-EXOMailboxStatistics -ExternalDirectoryObjectId $mbx.ExternalDirectoryObjectId -Properties LastLogonTime
        $row.ItemCount            = $stats.ItemCount
        $row.TotalItemSize        = "$($stats.TotalItemSize)"
        $row.TotalItemSizeBytes   = Get-CMBytes $stats.TotalItemSize
        $row.DeletedItemCount     = $stats.DeletedItemCount
        $row.TotalDeletedItemSize = "$($stats.TotalDeletedItemSize)"
        $row.LastLogonTime        = $stats.LastLogonTime
    }
    catch {
        $row.Error = "Statistics: $($_.Exception.Message)"
    }

    if ($row.ArchiveEnabled -and -not $SkipArchive) {
        try {
            $arch = Get-EXOMailboxStatistics -ExternalDirectoryObjectId $mbx.ExternalDirectoryObjectId -Archive
            $row.ArchiveItemCount          = $arch.ItemCount
            $row.ArchiveTotalItemSize      = "$($arch.TotalItemSize)"
            $row.ArchiveTotalItemSizeBytes = Get-CMBytes $arch.TotalItemSize
        }
        catch {
            $row.Error = ($row.Error + " Archive: $($_.Exception.Message)").Trim()
        }
    }

    $results.Add([pscustomobject]$row)
}
Write-Progress -Activity 'Reading mailbox statistics' -Completed

# 4. Export
try {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    $failed = @($results | Where-Object { $_.Error }).Count
    Write-Host ("Done. {0} mailboxes written, {1} with errors." -f $results.Count, $failed)
    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.