Back to script library
Entra / Microsoft 365 · Exchange Online

Report unused exo mailboxes

Find and report unused Exchange Online mailboxes.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -Scopes User.Read.All, AuditLog.Read.All -NoWelcome

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

Connect-MgGraph -Scopes User.Read.All, AuditLog.Read.All -NoWelcome
# Check for Exchange Online
$ModulesLoaded = Get-Module | Select-Object Name
If (!($ModulesLoaded -match "ExchangeOnlineManagement")) {
Write-Host "Loading Exchange Online PowerShell module" -ForegroundColor Yellow
Connect-ExchangeOnline -ShowBanner:$False
}
# Find mailboxes and check if they are unused
$Now = Get-Date -format s
[int]$i = 0
Write-Host "Looking for User Mailboxes..."
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited | `
Select-Object DisplayName, DistinguishedName, UserPrincipalName, ExternalDirectoryObjectId | Sort-Object DisplayName
Write-Host ("Reporting {0} mailboxes..." -f $Mbx.Count)
$Report = [System.Collections.Generic.List[Object]]::new()
ForEach ($M in $Mbx) {
$i++
Write-Host ("Processing {0} {1}/{2}" -f $M.DisplayName, $i, $Mbx.count)
$LastActive = $Null
$Log = Export-MailboxDiagnosticLogs -Identity $M.DistinguishedName -ExtendedProperties
$xml = [xml]($Log.MailboxLog)
$LastEMail = $Null; $LastCalendar = $Null; $LastContacts = $Null; $LastFile = $Null
$LastEmail = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastEmailTimeCurrentValue"}).Value
$LastCalendar = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastCalendarTimeCurrentValue"}).Value
$LastContacts = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastContactsTimeCurrentValue"}).Value
$LastFile = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastFileTimeCurrentValue"}).Value
$LastLogonTime = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastLogonTime"}).Value
$LastActive = ($xml.Properties.MailboxTable.Property | Where-Object {$_.Name -eq "LastUserActionWorkloadAggregateTime"}).Value
# This massaging of dates is to accommodate the different U.S. date format returned by Export-MailboxDiagnosticsData
[datetime]$LastActiveDateTime = Get-Date
If ([string]::IsNullOrEmpty($LastActive)) {
$DaysSinceActive = "N/A"
}
If (($LastActive.IndexOf("M") -gt -0)) { # U.S. format date with AM or PM in it
$LastActiveDateTime = [datetime]$LastActive
} Else {
$LastActiveDateTime = Get-Date ($LastActive)
}
If ($LastActiveDateTime) {
$DaysSinceActive = (New-TimeSpan -Start $LastActiveDateTime -End $Now).Days
}
# Get Mailbox statistics
$Stats = (Get-ExoMailboxStatistics -Identity $M.DistinguishedName)
$MbxSize = ($Stats.TotalItemSize.Value.ToString()).Split("(")[0]
# Get last Sign in from Entra ID sign in logs
$LastUserSignIn = $null
$LastUserSignIn = (Get-MgAuditLogSignIn -Filter "UserId eq '$($M.ExternalDirectoryObjectId)'" -Top 1).CreatedDateTime
If ($LastUserSignIn) {
$LastUserSignInDate = Get-Date($LastUserSignIn) -format g
} Else {
$LastUserSignInDate = "No sign in records found in last 30 days"
}
# Get account enabled status
$AccountEnabled = (Get-MgUser -UserId $M.ExternalDirectoryObjectId -Property AccountEnabled).AccountEnabled
$ReportLine = [PSCustomObject][Ordered]@{
Mailbox = $M.DisplayName
UPN = $M.UserPrincipalName
Enabled = $AccountEnabled
Items = $Stats.ItemCount
Size = $MbxSize
LastLogonExo = $LastLogonTime
LastLogonAD = $LastUserSignInDate
DaysSinceActive = $DaysSinceActive
LastActive = $LastActive
LastEmail = $LastEmail
LastCalendar = $LastCalendar
LastContacts = $LastContacts
LastFile = $LastFile }
$Report.Add($ReportLine)
}
$Report | Sort-Object DaysSinceActive -Descending | Out-GridView
# Extract the mailboxes that are inactive for more than 60 days but only take 25 because that's how much we can post in Teams
[array]$UnusedMailboxes = $Report | Where-Object {$_.DaysSinceActive -ge 60 } | Sort-Object DaysSinceActive -Descending | Select-Object -First 25
If ($UnusedMailboxes.Count -eq 0) {
Write-Host "No unused mailboxes found!" ; break
}
# The original script posted to a Teams channel. Changed in this version to email to an address
$MsgFrom = (Get-MgContext).Account
$MsgSubject = "Report of possibly unused Exchange Online mailboxes"
$ToRecipient = @{}
# Update the target email address here
$ToRecipient.Add("emailAddress",@{'address'="Help.Desk@office365itpros.com"})
[array]$MsgTo = $ToRecipient
$HtmlMsg = $UnusedMailboxes | Select-Object Mailbox, DaysSinceActive, LastEmail, LastActive, LastLogonExo, LastLogonAD | ConvertTo-Html -Fragment
# Construct the message body
$MsgBody = @{}
$MsgBody.Add('Content', "$($HtmlMsg)")
$MsgBody.Add('ContentType','html')
# Build the parameters to submit the message
$Message = @{}
$Message.Add('subject', $MsgSubject)
$Message.Add('toRecipients', $MsgTo)
$Message.Add('body', $MsgBody)
$EmailParameters = @{}
$EmailParameters.Add('message', $Message)
$EmailParameters.Add('saveToSentItems', $true)
$EmailParameters.Add('isDeliveryReceiptRequested', $true)
# Send the message
Try {
Write-Host "Sending email to $($MsgTo.emailAddress.address)" -ForegroundColor Yellow
Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters
} Catch {
Write-Host "Failed to send email to $($MsgTo.emailAddress.address)" -ForegroundColor Red
}
Attribution