To find unused mailboxes in Exchange Online, read the mailbox usage report and keep the mailboxes whose last activity date is older than your limit, such as 90 days. The report counts email and meeting activity only, and names mailboxes only when the tenant shows user names. Microsoft's inactive mailbox is a different thing: a deleted user's mailbox kept by a hold.
| Method | Best fit |
|---|---|
| PowerShell | A CSV of mailboxes with no activity for a limit you choose. |
| Admin centre | Microsoft's Mailbox usage report exported to CSV, then sorted by last activity date. |
| Altivane | Recommended for regular clean-ups, with each mailbox rated and its licence and forwarding shown. |
Before you start
To read the report mailbox by mailbox, use a role that shows user details, such as Reports Reader or Exchange Administrator; Global Reader sees tenant totals only. The PowerShell example requests Reports., which may need administrator consent.
Here, unused means a mailbox whose account still exists but has not been used for a long time. Microsoft's inactive mailbox, a deleted user's mailbox kept by a hold, is not covered.
The report usually covers up to the last 24 to 48 hours. Its last activity date is the last time someone sent or read email or worked with meetings, so mail arriving does not set it. The examples read and export and change nothing.
Option 1
Find unused mailboxes with PowerShell
Install the modules once. The script saves the report for the last 180 days to a temporary file, keeps mailboxes with no activity for 90 days or more, and exports them. Change $limitDays for another limit.
# Install once, if the modules are not already installed.
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Reports -Scope CurrentUser$ErrorActionPreference = 'Stop'
$limitDays = 90
$invariant = [System.Globalization.CultureInfo]::InvariantCulture
$utc = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor
[System.Globalization.DateTimeStyles]::AdjustToUniversal
# The report gives whole days in UTC. Blank or unreadable text gives $null.
function ConvertTo-UtcDay([string]$Text) {
$day = [datetime]::MinValue
if ([datetime]::TryParse($Text, $invariant, $utc, [ref]$day)) { return $day }
return $null
}
Connect-MgGraph -Scopes 'Reports.Read.All' -NoWelcome
$file = New-TemporaryFile
try {
Get-MgReportMailboxUsageDetail -Period D180 -OutFile $file.FullName
$rows = @(Import-Csv -Path $file.FullName)
if ($rows.Count -eq 0) { throw 'The usage report listed no mailboxes.' }
if (-not ($rows | Where-Object { $_.'User Principal Name' -like '*@*' })) {
throw 'The report conceals user names, so the mailboxes cannot be named.'
}
$now = (Get-Date).ToUniversalTime()
$report = @(foreach ($row in $rows) {
if ($row.'Is Deleted' -eq 'True') { continue }
$last = ConvertTo-UtcDay $row.'Last Activity Date'
$created = ConvertTo-UtcDay $row.'Created Date'
$idle = if ($last) { [int][math]::Floor(($now - $last).TotalDays) } else { $null }
if ($null -ne $idle -and $idle -lt $limitDays) { continue }
$isNew = $created -and ($now - $created).TotalDays -lt $limitDays
if ($null -eq $idle -and $isNew) { continue }
[pscustomobject]@{
DisplayName = $row.'Display Name'
UserPrincipalName = $row.'User Principal Name'
LastActivityDate = if ($last) { $last.ToString('yyyy-MM-dd') } else { '' }
DaysInactive = $idle
Created = if ($created) { $created.ToString('yyyy-MM-dd') } else { '' }
ItemCount = $row.'Item Count'
SizeGB = [math]::Round([double]$row.'Storage Used (Byte)' / 1GB, 2)
}
})
if ($report.Count -eq 0) {
Write-Output "No mailbox has been unused for $limitDays days or more."
} else {
$byIdle = { if ($null -eq $_.DaysInactive) { [int]::MaxValue } else { $_.DaysInactive } }
$report | Sort-Object -Property $byIdle -Descending |
Export-Csv -Path './unused-mailboxes.csv' -NoTypeInformation -Encoding utf8
$asOf = $rows[0].'Report Refresh Date'
Write-Output "$($report.Count) mailboxes exported. Report data as of $asOf."
}
} finally {
Remove-Item -Path $file.FullName -ErrorAction SilentlyContinue
Disconnect-MgGraph | Out-Null
}The CSV lists each mailbox past the limit, longest idle first. A mailbox with no date comes first unless it is newer than the limit. The report mixes user and shared mailboxes, and a mailbox it does not list cannot appear.
Check a single mailbox
After Connect-ExchangeOnline, read Exchange's own last logon time for 1 mailbox.
# Run while connected to Exchange Online. Replace the example mailbox.
Get-EXOMailboxStatistics -Identity 'person@example.com' -Properties LastLogonTime |
Select-Object DisplayName,LastLogonTimeLastLogonTime records logons, not email activity, so it can differ from the report. Do not use LastUserActionTime, which Microsoft is deprecating.
Option 2
Export Microsoft's Mailbox usage report
- Open the Microsoft 365 admin centre for the correct tenant.
- Go to Reports > Usage, and under Reports select Exchange.
- On the Email activity tab, select View More, then choose Exchange > Mailbox usage from the list.
- Choose Export to download the usage of every mailbox as a CSV file.
- In Excel, sort by last activity date, oldest first, and use the recipient type and deleted columns to filter.
This suits a one-off look at a small tenant. It does not rate mailboxes against a limit.
Option 3 ยท Recommended for regular clean-ups
Run Altivane's Inactive Mailboxes report
- Open Altivane and choose the tenant.
- Set Inactive days, starting at 90, and run the Inactive Mailboxes report for Exchange Online.
- Open the Excel workbook the run wrote and read its rows, including Status, Days inactive, Last activity, Activity source, Licensed and Forwarding.
The report lists user and shared mailboxes, not rooms, equipment or Bookings, and leaves out mailboxes used within the limit and new ones not yet used. Dates come from the same usage report, which needs Reports.; where it has none, Altivane uses Exchange's last interaction or last logon time, and Activity source says which. Status is Warning for a user mailbox past the limit and Review for a shared mailbox or one with no activity recorded.
Common questions
Is an unused mailbox the same as an inactive mailbox?
No. Microsoft's inactive mailbox is a soft-deleted mailbox kept by a hold after its user's account was deleted; an unused mailbox belongs to a live account. Altivane lists the unused kind in its Inactive Mailboxes report and mailboxes kept by a hold in its Deleted Mailboxes report.
Why does the script stop with a message about user names?
Usage reports hide user names by default. A Global Administrator can show them in the admin centre: go to Settings > Org settings > Services > Reports, clear Conceal user, group, and site names in all reports and select Save. The setting applies to every usage report in the tenant, and showing names is recorded in the Microsoft Purview audit log. Tick the box again to hide them.
Does no mailbox activity mean the person has left?
No. The date counts email and meeting activity, so someone who works only in Teams or SharePoint can look unused. A shared mailbox has no activity independent of a user mailbox, so ask its users before removing it. For sign-ins, see how to find inactive users in Microsoft Entra ID.
Microsoft references
Technical references checked on 5 October 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.
- Get-
Mg Report Mailbox Usage Detail reference - reportRoot: getMailboxUsageDetail in Microsoft Graph
- Authorization for APIs to read Microsoft 365 usage reports
- Exchange Mailbox usage report
- Microsoft 365 admin centre usage reports overview
- Get-EXOMailboxStatistics reference
- Get-MailboxStatistics reference
- Learn about inactive mailboxes