Entra / Microsoft 365 · Exchange Online
Report mailbox items graph
A script to demonstrate how to use Graph API queries to read the folders and folder 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 = "",[string] $AppId = "",[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 Get-GraphData {# Based on https://danielchronlund.com/2018/11/19/fetch-data-from-microsoft-graph-with-powershell-paging-support/# GET data from Microsoft Graph.param ([parameter(Mandatory = $true)]$AccessToken,[parameter(Mandatory = $true)]$Uri)# Check if authentication was successful.if ($AccessToken) {$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $AccessToken"'ConsistencyLevel' = "eventual" }# Create an empty array to store the result.$QueryResults = @()# Invoke REST method and fetch data until there are no pages left.do {$Results = ""$StatusCode = ""do {try {$Results = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method "GET" -ContentType "application/json"$StatusCode = $Results.StatusCode} catch {$StatusCode = $_.Exception.Response.StatusCode.value__if ($StatusCode -eq 429) {Write-Warning "Got throttled by Microsoft. Sleeping for 45 seconds..."Start-Sleep -Seconds 45}else {Write-Error $_.Exception}}} while ($StatusCode -eq 429)if ($Results.value) {$QueryResults += $Results.value}else {$QueryResults += $Results}$uri = $Results.'@odata.nextlink'} until (!($uri))# Return the result.$QueryResults}else {Write-Error "No Access Token"}}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 FunctionsClear-Host# Check that we have the necessary Exchange Online module loaded$ModulesLoaded = Get-Module | Select-Object NameIf (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break}# Set these values to those appropriate in your tenant (these values won't work)$AppSecret = "bzS8Q~9EDXMUrUOJUbZXTTiJp7lTFdkWskETObRU"# Make sure that the app has the Mail.Read.All application permission# Construct URI and body needed for authentication$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"$body = @{client_id = $AppIdscope = "https://graph.microsoft.com/.default"client_secret = $AppSecretgrant_type = "client_credentials"}# Get OAuth 2.0 Token$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing# Unpack Access Token$global:token = ($tokenRequest.Content | ConvertFrom-Json).access_token$Headers = @{'Content-Type' = "application\json"'Authorization' = "Bearer $Token"'ConsistencyLevel' = "eventual" }If (!($Token)) { Write-Host "Can't get Azure AD access token - exiting" ; break }# 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"$Mailbox = Read-Host "Enter the name of the mailbox to report"$YearAgo = (Get-Date).AddDays(-$LookbackDays)$YearAgo = (Get-Date -Date $YearAgo -Format s) + "Z"[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 }[datetime]$StartTime = Get-Date$FolderList = [System.Collections.Generic.List[Object]]::new()Write-Host ("Checking mailbox information for mailbox {0} ({1})" -f $Mbx.DisplayName, $Mbx.UserPrincipalName)# Get list of folders in the mailbox$Uri = ("https://graph.microsoft.com/v1.0/users/{0}/MailFolders?includeHiddenFolders=true&`$Top=500" -f $Mbx.ExternalDirectoryObjectId)[array]$AllFolders = Get-GraphData -Uri $Uri -AccessToken $Token# Get list of folders that we want to process$AllFolders = $AllFolders | Where-Object {$_.displayName -Notin $FoldersToIgnore} | Sort-Object DisplayName# 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)} #End Foreach ChildFolder}# Remove any blank folders that might have been returned by the Graph$FolderList = $FolderList | Where-Object {$_.DisplayName -ne $Null }| Sort-Object DisplayNameClear-HostIf (!($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 = $Folder.DisplayName + " [ Folder "+ $i +"/" + $FolderList.Count + " Items found so far " + $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$Uri = 'https://graph.microsoft.com/v1.0/users/' + $Mbx.ExternalDirectoryObjectId + "/mailfolders/" + $Folder.Id + `"/Messages/?select=receivedDateTime,subject,sender,id,importance,torecipients,ccrecipients,bccrecipients,bodypreview&`$filter=receivedDateTime le $($YearAgo)&`$expand=singleValueExtendedProperties(`$filter=Id%20eq%20'LONG%200x0E08')"[array]$Messages = Get-GraphData -Uri $Uri -AccessToken $TokenIf (!($Messages[0].'@odata.context')) { # Check that the Graph has returned some messages or just a note that it hasn't found anythingWrite-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 - $StartTimeWrite-Host ("Elapsed time {0}" -f $Elapsed)Write-Host ""Write-Host ("{0} messages found" -f $ReportList.Count)$ExcelFileName = "c:\temp\MailboxData_" + $Mbx.ExternalDirectoryObjectId + ".xlsx"$ExcelTitle = "Mailbox Data for " + $Mbx.DisplayName$WorkSheetTitle = "Created " + (Get-Date -format "dd-MMM-yyyy hhmm")$ReportList | Export-Excel -Path $ExcelFileName -WorksheetName $WorkSheetTitle -Title $ExcelTitle -TitleBold -TableName "MailboxData" -TableStyle Medium28Write-Host "Output Excel spreadsheet is available in" $ExcelFileName
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.-LookbackDays365Number of days back to search the unified audit log.Attribution
Author
Office365itpros