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 NameIf (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Loading Exchange Online PowerShell module" -ForegroundColor YellowConnect-ExchangeOnline -ShowBanner:$False}# Find mailboxes and check if they are unused$Now = Get-Date -format s[int]$i = 0Write-Host "Looking for User Mailboxes..."[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited | `Select-Object DisplayName, DistinguishedName, UserPrincipalName, ExternalDirectoryObjectId | Sort-Object DisplayNameWrite-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-DateIf ([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).CreatedDateTimeIf ($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.DisplayNameUPN = $M.UserPrincipalNameEnabled = $AccountEnabledItems = $Stats.ItemCountSize = $MbxSizeLastLogonExo = $LastLogonTimeLastLogonAD = $LastUserSignInDateDaysSinceActive = $DaysSinceActiveLastActive = $LastActiveLastEmail = $LastEmailLastCalendar = $LastCalendarLastContacts = $LastContactsLastFile = $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 25If ($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 messageTry {Write-Host "Sending email to $($MsgTo.emailAddress.address)" -ForegroundColor YellowSend-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters} Catch {Write-Host "Failed to send email to $($MsgTo.emailAddress.address)" -ForegroundColor Red}
Attribution
Author
Office365itpros