Entra / Microsoft 365 · Exchange Online
Report mailbox messages (SDK)
Use Microsoft Graph PowerShell SDK queries to read folders and items from an Exchange Online mailbox.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint -NoWelcome
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "Your Tenant ID",[string] $AppId = "Your app ID",[int] $LookbackDays = 365)function FormatFileSize {# Format File Size nicelyparam ([parameter(Mandatory = $true)]$InFileSize)If ($InFileSize -lt 1KB) { # Format the size of a document$FileSize = $InFileSize.ToString() + " B" }ElseIf ($InFileSize -lt 1MB) {$FileSize = $InFileSize / 1KB$FileSize = ("{0:n2}" -f $FileSize) + " KB"}Elseif ($InFileSize -lt 1GB) {$FileSize = $InFileSize / 1MB$FileSize = ("{0:n2}" -f $FileSize) + " MB" }Elseif ($InFileSize -ge 1GB) {$FileSize = $InFileSize / 1GB$FileSize = ("{0:n2}" -f $FileSize) + " GB" }Return $FileSize}Function FormatDuration ([TimeSpan]$Duration) {$h = $Duration.Days * 24 + $Duration.Hours$m = $Duration.Minutesswitch ($h) {0 {$HourStr=$null}1 {$HourStr="$h hour"}{$_ -ge 1} {$HourStr="$h hours"}}switch ($m) {0 {$MinuteStr=$null}1 {$MinuteStr="$m minute"}{$_ -ge 1} {$MinuteStr="$m minutes"}}if ($HourStr -gt 0 -and $MinuteStr -gt 0) {[String]$DurationStr=$HourStr+" "+$MinuteStr} else {$DurationStr=$HourStr+$MinuteStr}return $DurationStr}function Get-SubFoldersRecursive {param ([Parameter(Mandatory = $true)][string]$FolderId,[Parameter(Mandatory = $true)][string]$UserId)$SubFolders = @()# Get direct child folders[array]$ChildFolders = Get-MgUserMailFolderChildFolder -UserId $UserId -MailFolderId $FolderId -Allforeach ($Child in $ChildFolders) {$SubFolders += $Child# Recursively get subfolders of this child$SubFolders += Get-SubFoldersRecursive -FolderId $Child.Id -UserId $UserId}return $SubFolders}# End Functions# Define your app ID and tenant ID here. The app must have the Mail.ReadWrite permission assigned to it in Entra ID$Thumbprint = "32C9529B1FFD08BCD483A5D98807E47A472C5318"Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint# Define the set of folders that we're not interested in processing[array]$FoldersToIgnore = "Deleted Items", "Sync Issues", "Conversation History", "Server Failures", "Local Failures", "Drafts", "Conflicts", "Junk Email", "Social Activity Notifications"$YearAgo = (Get-Date).AddDays(-$LookbackDays)$YearAgo = (Get-Date -Date $YearAgo -Format s) + "Z"Clear-Host# Check that we have the necessary Exchange Online module loaded[array]$Modules = Get-Module | Select-Object -ExpandProperty NameIf ("ExchangeOnlineManagement" -notin $Modules) {Connect-ExchangeOnline -ShowBanner:$False}# Who are we processing?$Mailbox = Read-Host "Enter the name of the mailbox to report"[array]$Mbx = Get-ExoMailbox -Identity $Mailbox -ErrorAction SilentlyContinueIf (!($Mbx)) {Write-Host ("A mailbox called {0} cannot be found - exiting" -f $Mailbox); break}If ($Mbx.RecipientTypeDetails -ne 'UserMailbox') {Write-Host "This script can only process user mailboxes - exiting"; break} Else {Write-Host ("Processing mailbof for {0}" -f $Mbx.DisplayName)}[datetime]$StartTime = Get-Date#$Uri = ("https://graph.microsoft.com/v1.0/users/{0}/MailFolders?includeHiddenFolders=true&`$Top=500" -f $Mbx.ExternalDirectoryObjectId)#[array]$AllFolders = Invoke-MgGraphRequest -Uri $Uri -Method Get | Select-Object -ExpandProperty Value[array]$AllFolders = Get-MgUserMailFolder -UserId $userId -IncludeHiddenFolders:$true -AllIf ($AllFolders.Count -eq 0) {Write-Host "No folders found in the mailbox - exiting"; break} Else {$AllFolders = $AllFolders | Where-Object {$_.displayName -Notin $FoldersToIgnore} | Sort-Object DisplayNameWrite-Host ("Found {0} folders to process in the mailbox" -f $AllFolders.Count)$FolderList = [System.Collections.Generic.List[Object]]::new()}# Build a table of folder ids and display namesForEach ($F in $AllFolders) {$FolderLine = [PSCustomObject][Ordered]@{Id = $F.IdDisplayName = $F.DisplayName }$FolderList.Add($FolderLine)}# Find folders with child folders[array]$FoldersWithChildFolders = $AllFolders | Where-Object {$_.ChildFolderCount -gt 0}ForEach ($ChildFolder in $FoldersWithChildFolders) {[array]$ChildFolders = Get-SubFoldersRecursive -FolderId $ChildFolder.Id -UserId $Mbx.ExternalDirectoryObjectIdForEach ($ChildFolderProcess in $ChildFolders) {$FolderLine = [PSCustomObject][Ordered]@{Id = $ChildFolderProcess.IdDisplayName = $ChildFolderProcess.DisplayName }$FolderList.Add($FolderLine)}}# Remove any blank folders that might have been returned by the Graph$FolderList = $FolderList | Where-Object {$null -ne $_.DisplayName} | Sort-Object DisplayNameIf (!($FolderList)) {Write-Host ("Can't find any folders in the {0} mailbox - script exiting..." -f $Mbx.displayname); break}$ReportList = [System.Collections.Generic.List[Object]]::new()$ProgDelta = 100/($FolderList.Count); $i = 0; $Progress = 0ForEach ($Folder in $FolderList) {$i++$Status = ("Processing folder: {0} ({1} of {2}. {3} items processed so far)" -f $Folder.DisplayName, $i, $FolderList.Count, $ReportList.count)Write-Progress -Activity "Reporting items in folder more than a year old" -Status $Status -PercentComplete $Progress$Progress += $ProgDelta# Find messages from a year ago[array]$Messages = Get-MgUserMailFolderMessage -UserId $Mbx.ExternalDirectoryObjectId -MailFolderId $Folder.id -All -PageSize 999 `-Property id, sender, receivedDateTime, createdDateTime, importance, subject, torecipients, ccrecipients,bccrecipients,bodypreview `-ExpandProperty "singleValueExtendedProperties(`$filter=Id eq 'LONG 0x0E08')" `-Filter "receivedDateTime le $($YearAgo)" `If ($Messages) {Write-Host ("Found {0} messages in the {1} folder" -f $Messages.count, $Folder.DisplayName)ForEach ($Message in $Messages) {$CcRecipients = $Null; $ToRecipients = $Null$BodyText = ($Message.BodyPreview -replace '</p>',"`r`n" -replace "<[^>]+>",'' -replace " ",' ').trim()If ($Message.torecipients.emailaddress.name.count -gt 0) {$ToRecipients = $Message.torecipients.emailaddress.name -join ", "} Else {$ToRecipients = $Message.torecipients.emailaddress.name}If ($Message.ccrecipients.emailaddress.name.count -gt 0) {$CcRecipients = $Message.ccrecipients.emailaddress.name -join ", "} Else {$CcRecipients = $Message.ccrecipients.emailaddress.name}[long]$Size = $Message.singleValueExtendedProperties.value$MsgSize = FormatFileSize $Size# Add each message to the list$ReportLine = [PSCustomObject][Ordered]@{Folder = $Folder.DisplayNameReceived = $Message.receivedDateTimeSubject = $Message.subjectTo = $ToRecipientsCC = $CcRecipientsSender = $message.sender.emailaddress.addressSenderName = $message.sender.emailaddress.nameImportance = $Message.importanceSize = $MsgSizeBody = $BodyTextid = $Message.IdFolderId = $Folder.IdUserId = $Mbx.ExternalDirectoryObjectId }$ReportList.Add($ReportLine)} # End Foreach Message} # End if Messages} # End Foreach Folder[datetime]$EndTime = Get-Date$Elapsed = $EndTime - $StartTime$Elapsed = FormatDuration $ElapsedWrite-Host ("Elapsed time {0}" -f $Elapsed)Write-Host ""Write-Host ("{0} messages found" -f $ReportList.Count)Write-Host "Generating report..."If (Get-Module ImportExcel -ListAvailable) {$ExcelGenerated = $TrueImport-Module ImportExcel -ErrorAction SilentlyContinue$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\MailboxData_" + $Mbx.ExternalDirectoryObjectId + ".xlsx"If (Test-Path $ExcelOutputFile) {Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue}$ReportList | Export-Excel -Path $ExcelOutputFile -WorksheetName "Mailbox Items" `-Title ("Mailbox Items {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "MailboxItems"} Else {$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\MailboxData_" + $Mbx.ExternalDirectoryObjectId + ".CSV"$ReportList | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8}If ($ExcelGenerated) {Write-Host ("An Excel report of calendar items is available in {0}" -f $ExcelOutputFile)} Else {Write-Host ("A CSV report of calendar items is available in {0}" -f $CSVOutputFile)}Write-Host "All done..."
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.-LookbackDays365Include mailbox items older than this many days in the report.Attribution
Author
Office365itpros