PowerShell: Export All Office 365 Mailbox Sizes to CSV
A script to export every mailbox size, item count, and archive size in your Microsoft 365 tenant to a CSV file for capacity planning and reporting.
5 min readUpdated March 29, 2026
Overview
Knowing how much storage each mailbox consumes is essential for capacity planning, licence decisions (switching between 50GB and 100GB plans), migration estimates, and identifying users who need to archive or clean up their mail.
This script connects to Exchange Online and exports every mailbox with its total size, item count, archive size, and last activity date to a CSV file.
Prerequisites
- Exchange Online Management module:
Install-Module ExchangeOnlineManagement - Exchange Administrator or Global Reader role in Microsoft 365
- PowerShell 5.1 or later (PowerShell 7.6 LTS recommended)
The Script
powershell
<#
.SYNOPSIS
Export all Office 365 mailbox sizes to CSV.
.DESCRIPTION
Retrieves mailbox statistics for all user mailboxes in Exchange Online,
including archive mailboxes, and exports to a timestamped CSV file.
.PARAMETER OutputPath
Directory to save the CSV. Defaults to the current directory.
.PARAMETER IncludeShared
Include shared mailboxes in the report.
.PARAMETER IncludeArchive
Include archive mailbox sizes (slower — requires an extra query per user).
#>
param(
[string]$OutputPath = ".",
[switch]$IncludeShared,
[switch]$IncludeArchive
)
# Connect to Exchange Online
Write-Host "Connecting to Exchange Online..." -ForegroundColor Yellow
Connect-ExchangeOnline -ShowBanner:$false
# Get mailboxes
$recipientTypes = @("UserMailbox")
if ($IncludeShared) {
$recipientTypes += "SharedMailbox"
}
Write-Host "Fetching mailboxes..." -ForegroundColor Yellow
$mailboxes = Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails $recipientTypes | Select-Object DisplayName, UserPrincipalName, PrimarySmtpAddress, RecipientTypeDetails, ArchiveStatus
$total = $mailboxes.Count
Write-Host "Found $total mailboxes. Collecting statistics..." -ForegroundColor Cyan
$results = [System.Collections.Generic.List[object]]::new()
$i = 0
foreach ($mb in $mailboxes) {
$i++
$pct = [math]::Round(($i / $total) * 100)
Write-Progress -Activity "Collecting mailbox stats" -Status "$i of $total ($pct%)" -PercentComplete $pct
try {
$stats = Get-MailboxStatistics -Identity $mb.UserPrincipalName -ErrorAction Stop
# Parse size string to bytes for sorting
$sizeString = $stats.TotalItemSize.Value.ToString()
$sizeBytes = $stats.TotalItemSize.Value.ToBytes()
$sizeMB = [math]::Round($sizeBytes / 1MB, 2)
$sizeGB = [math]::Round($sizeBytes / 1GB, 2)
$record = [PSCustomObject]@{
DisplayName = $mb.DisplayName
Email = $mb.PrimarySmtpAddress
Type = $mb.RecipientTypeDetails
TotalSizeGB = $sizeGB
TotalSizeMB = $sizeMB
ItemCount = $stats.ItemCount
DeletedItemSizeMB = [math]::Round($stats.TotalDeletedItemSize.Value.ToBytes() / 1MB, 2)
DeletedItemCount = $stats.DeletedItemCount
LastLogonTime = $stats.LastLogonTime
ArchiveStatus = $mb.ArchiveStatus
ArchiveSizeGB = ""
ArchiveItemCount = ""
}
# Get archive stats if requested
if ($IncludeArchive -and $mb.ArchiveStatus -eq "Active") {
try {
$archiveStats = Get-MailboxStatistics -Identity $mb.UserPrincipalName -Archive -ErrorAction Stop
$record.ArchiveSizeGB = [math]::Round($archiveStats.TotalItemSize.Value.ToBytes() / 1GB, 2)
$record.ArchiveItemCount = $archiveStats.ItemCount
} catch {
$record.ArchiveSizeGB = "Error"
}
}
$results.Add($record)
} catch {
Write-Warning "Failed to get stats for $($mb.DisplayName): $_"
$results.Add([PSCustomObject]@{
DisplayName = $mb.DisplayName
Email = $mb.PrimarySmtpAddress
Type = $mb.RecipientTypeDetails
TotalSizeGB = "Error"
TotalSizeMB = "Error"
ItemCount = ""
DeletedItemSizeMB = ""
DeletedItemCount = ""
LastLogonTime = ""
ArchiveStatus = $mb.ArchiveStatus
ArchiveSizeGB = ""
ArchiveItemCount = ""
})
}
}
Write-Progress -Activity "Collecting mailbox stats" -Completed
# Sort by size descending
$sorted = $results | Sort-Object { if ($_.TotalSizeGB -is [double]) { $_.TotalSizeGB } else { 0 } } -Descending
# Export to CSV
$timestamp = Get-Date -Format "yyyy-MM-dd_HHmm"
$filename = "MailboxSizes_$timestamp.csv"
$filepath = Join-Path $OutputPath $filename
$sorted | Export-Csv -Path $filepath -NoTypeInformation -Encoding UTF8
Write-Host "`nExported to: $filepath" -ForegroundColor Green
# Summary
$totalSizeGB = ($results | Where-Object { $_.TotalSizeGB -is [double] } | Measure-Object -Property TotalSizeGB -Sum).Sum
$totalItems = ($results | Where-Object { $_.ItemCount -is [int] } | Measure-Object -Property ItemCount -Sum).Sum
Write-Host "`n--- Summary ---" -ForegroundColor Cyan
Write-Host "Mailboxes: $total"
Write-Host "Total Size: $([math]::Round($totalSizeGB, 2)) GB"
Write-Host "Total Items: $($totalItems.ToString('N0'))"
Write-Host "Largest: $($sorted[0].DisplayName) ($($sorted[0].TotalSizeGB) GB)"
Disconnect-ExchangeOnline -Confirm:$false
Usage
Basic export (user mailboxes only)
powershell
.\Export-MailboxSizes.ps1
Include shared mailboxes
powershell
.\Export-MailboxSizes.ps1 -IncludeShared
Include archive sizes
powershell
.\Export-MailboxSizes.ps1 -IncludeShared -IncludeArchive
Export to a specific directory
powershell
.\Export-MailboxSizes.ps1 -OutputPath "C:\Reports"
Output Example
| DisplayName | Type | TotalSizeGB | ItemCount | ArchiveStatus | |
|---|---|---|---|---|---|
| Jane Smith | [email protected] | UserMailbox | 45.2 | 52,341 | Active |
| John Doe | [email protected] | UserMailbox | 38.7 | 41,208 | None |
| Reception | [email protected] | SharedMailbox | 12.1 | 15,432 | None |
Scheduling This Report
Run monthly via Task Scheduler
powershell
# Create a scheduled task to run on the 1st of each month
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File C:\Scripts\Export-MailboxSizes.ps1 -IncludeShared -OutputPath C:\Reports"
$trigger = New-ScheduledTaskTrigger -Monthly -DaysOfMonth 1 -At 6am
Register-ScheduledTask -TaskName "Monthly Mailbox Report" -Action $action -Trigger $trigger -RunLevel Highest
Tip
For tenants with over 1,000 mailboxes, this script can take 15-30 minutes due to the per-mailbox statistics query. Run it outside business hours to avoid throttling.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| "Access denied" | Insufficient Exchange Online permissions | Assign Exchange Administrator or Global Reader role |
| Throttling errors | Too many queries in a short period | The script uses sequential queries — retry after a few minutes |
| Empty archive size | Archive not enabled for the user | Enable archive: Enable-Mailbox -Identity [email protected] -Archive |
| "Mailbox not found" | Licence removed or mailbox deleted | Skip — the error is logged in the CSV as "Error" |
PowerShellExchange OnlineMicrosoft 365Reporting
Related Articles
Coding & AutomationPowerShell: Automate New User Provisioning in Active Directory and Microsoft 365A comprehensive PowerShell script to create new users in Active Directory and assign Microsoft 365 licences, mailboxes, and group memberships in one step.Read SecurityIncident Response Checklist for a Compromised Office 365 AccountA step-by-step incident response checklist for when a Microsoft 365 account is compromised. Covers containment, investigation, remediation, and prevention.Read
Was this article helpful?
