Skip to content

MailboxAudit

vaarg edited this page Jun 18, 2026 · 8 revisions

Invoke-MailboxAudit.ps1

A PowerShell supplement to GraphRunner that probes user mailboxes and Microsoft 365 group inboxes for read access, then reads or searches messages from those that are accessible. Designed to be chained with Resume-GroupAudit.ps1 output for group inbox discovery.

Background

GraphRunner's built-in mailbox functions have three problems:

  • Invoke-GraphOpenInboxFinder fails silently on every error. The catch block sets $err = $_.Exception.Response.StatusCode.Value__ but never references $err anywhere. Every HTTP 401, 403, 404, and 429 response produces no output. The caller has no way to know whether a mailbox was inaccessible, the token expired, or the run was being rate-limited.
  • No token refresh. A long user list will hit token expiry mid-run with no recovery.
  • No group inbox support. GraphRunner's mailbox functions only target user mailboxes. Microsoft 365 groups have a separate conversations inbox endpoint that existing functions do not cover.

Invoke-MailboxAudit.ps1 fixes the first two issues and adds group inbox support. It splits the work into three independent functions: a probe phase that identifies accessible mailboxes and records them to a CSV, a read phase that ingests that CSV and retrieves or searches messages (with optional body and attachment saving), and an offline search phase that searches previously saved bodies without making any further API calls.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7+
  • GraphRunner dot-sourced in the same session (provides Invoke-RefreshGraphTokens)
  • A valid Graph access token obtained via GraphRunner's Get-GraphTokens or Invoke-RefreshGraphTokens
  • Mail.Read.Shared or Mail.ReadWrite.Shared scope to read other users' mailboxes
  • Group.Read.All or membership in the group to read group conversations

Setup

# Dot-source GraphRunner first, then this file
. .\GraphRunner\GraphRunner.ps1
. .\Invoke-MailboxAudit.ps1

Encoding note: The file is pure ASCII. If you edit it, save as ASCII or UTF-8 with BOM. Windows PowerShell 5.1 reads files without a BOM as Windows-1252 by default, which silently corrupts multi-byte Unicode characters (em-dashes, smart quotes, etc.) and causes parse errors.


Workflow Overview

Step 1 - Test-MailboxAccess
    Probe user mailboxes and M365 group inboxes -> AccessibleMailboxes.csv
                                                -> AccessibleMailboxes_user_ids.txt
                                                -> AccessibleMailboxes_group_ids.txt
            |
            v
Step 2 - Get-MailboxMessages
    Read or search messages from accessible mailboxes
    -> console preview + optional OutFile CSV (appended per mailbox)
    -> optional saved body files + attachments (-SaveTo, -SaveAttachments)
            |
            v  (optional, after a -SaveTo run)
Step 3 - Search-MailboxCache
    Search previously saved bodies offline -- zero API calls
    -> console preview + optional OutFile CSV

The three steps are independent. If Test-MailboxAccess is interrupted, the fallback _ids.txt files contain whatever was found accessible up to that point. Get-MailboxMessages only requires a valid AccessibleMailboxes.csv and can be re-run against the same file with different parameters (different search terms, different -Type filters, etc.) without re-probing. Search-MailboxCache only requires either the CSV index or the saved body folder and generates no API traffic, making it preferable for repeated searches once bodies have been downloaded.


Authentication

Authenticate using GraphRunner before running either function. The ClientID used matters — the refresh token is bound to the client it was issued for, and refreshing with a different ClientID returns HTTP 400.

# Example using the Azure Portal client
Invoke-RefreshGraphTokens -TenantID "contoso.com" `
    -ClientID "04b07795-8ddb-461a-bbee-02f9e1bf7b46" `
    -RefreshToken "0.A..."

Both functions auto-detect the ClientID from the appid claim in the access token JWT, so you do not need to pass -ClientID explicitly unless you want to override it.


Step 1 — Test-MailboxAccess

Probes user mailboxes via GET /users/{id}/mailFolders/Inbox/messages?$top=1 and Microsoft 365 group inboxes via GET /groups/{id}/conversations?$top=1 to determine which are readable by the current token. Both types are probed in a single run by default; use -CheckUsersOnly or -CheckGroupsOnly to narrow the scope.

For users, a readable inbox is a notable finding — it indicates a misconfigured shared mailbox. For groups, inbox access may be expected if the token belongs to a group member, but the content is still worth reviewing.

Token handling mirrors Resume-GroupAudit.ps1: the ClientID is auto-detected from the JWT appid claim, proactive refreshes run on a configurable interval, 401 responses trigger an immediate refresh-and-retry (up to 3 attempts), and 429 responses respect the Retry-After header without consuming the retry budget.

Group type note: Only Microsoft 365 (Unified) groups have a conversations inbox. Security groups, Distribution groups, and Mail-enabled security groups will return HTTP 400 or 404. These are logged explicitly as Not a mail-enabled M365 group rather than silently skipped. If your group input comes from Resume-GroupAudit.ps1, the groupType column in updatable_details.csv lets you pre-filter to Microsoft 365 groups before passing to this function.

Parameters

User inputs (compat with GraphRunner's users.txt format):

Parameter Required Default Description
-Tokens Yes Token object from GraphRunner
-UserList One of Text file with one UPN or email per line
-UserIds One of String array of UPNs or email addresses

Group inputs (compat with Resume-GroupAudit.ps1 output files):

Parameter Required Default Description
-InputCsv One of CSV with at minimum an id column. Accepts updatable.csv, updatable_details.csv, or any CSV from Test-GraphGroupMemberAccess
-InputFile One of Text file in DisplayName:GUID format, e.g. updatable_ids.txt
-GroupIds One of String array of group GUIDs

Scope:

Parameter Required Default Description
-CheckUsersOnly No Probe only user mailboxes; skip group inputs
-CheckGroupsOnly No Probe only group inboxes; skip user inputs

Resume — user list (use one or the other, not both):

Parameter Required Default Description
-SkipUsers No 0 Skip the first N users (resume by position)
-StartFromUser No Skip all users before this UPN (resume by ID)

Resume — group list (use one or the other, not both):

Parameter Required Default Description
-SkipGroups No 0 Skip the first N groups (resume by position)
-StartFromGroup No Skip all groups before this GUID (resume by ID)

Output and token handling:

Parameter Required Default Description
-OutputFile No AccessibleMailboxes.csv Path for the unified results CSV
-RefreshInterval No 300 Seconds between proactive token refreshes
-ClientID No Auto-detected Override the client ID used for token refresh

Output files

File Description
<OutputFile> Unified CSV for all probed mailboxes, written at the end. Contains both accessible and inaccessible entries.
<stem>_user_ids.txt Appended incrementally: DisplayName:UPN per accessible user mailbox as results come in. Survives a crash mid-run.
<stem>_group_ids.txt Appended incrementally: DisplayName:GUID per accessible group inbox as results come in. Survives a crash mid-run.

The unified CSV uses these columns:

Column Description
Type User or Group
Id Object ID (UPN for users, GUID for groups)
DisplayName Display name
MailAddress UPN (users) or group mail address if known (groups)
Accessible True or False
HttpStatus HTTP response code
Result Accessible, Access denied, Not found or no mailbox, Not a mail-enabled M365 group, Authentication failed, Request failed (HTTP N), or Max retries exceeded
LatestSubject Subject of the most recent message (accessible mailboxes) or (empty inbox) / (no conversations)
LatestSender Sender address (users) or sender display name from uniqueSenders (groups)
LatestDate receivedDateTime (users) or lastDeliveredDateTime (groups)

Resuming after failure

When a mailbox exhausts all retries, the console prints a resume hint:

[!] Giving up on 'user@contoso.com' after 3 attempts.
    Resume tip: -StartFromUser 'user@contoso.com'

[!] Giving up on 'Finance Team' (xxxxxxxx-...) after 3 attempts.
    Resume tip: -StartFromGroup 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

User and group resume controls are independent — a failure partway through the group list does not affect the user counter, and vice versa.

# Resume user probe from a known address
Test-MailboxAccess -Tokens $tokens -UserList .\users.txt -StartFromUser "jsmith@contoso.com"

# Resume group probe from a known GUID
Test-MailboxAccess -Tokens $tokens -InputCsv .\updatable_details.csv -StartFromGroup "xxxxxxxx-..."

# Resume both at once
Test-MailboxAccess -Tokens $tokens `
    -UserList       .\users.txt `
    -InputCsv       .\updatable_details.csv `
    -StartFromUser  "jsmith@contoso.com" `
    -StartFromGroup "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
    -OutputFile     .\AccessibleMailboxes_resumed.csv

If the script crashes before writing the final CSV, the _user_ids.txt and _group_ids.txt fallback files contain whatever was found accessible up to the crash. Pass them directly as -InputFile and -UserList (after stripping to just UPNs for users) to Get-MailboxMessages, or re-run Test-MailboxAccess using the resume flags to finish the probe.

Examples

# Probe user mailboxes from GraphRunner users.txt and M365 group inboxes from GroupAudit
Test-MailboxAccess -Tokens $tokens -UserList .\users.txt -InputCsv .\updatable_details.csv

# Probe groups only, using the summary CSV
Test-MailboxAccess -Tokens $tokens -InputCsv .\updatable.csv -CheckGroupsOnly

# Probe groups only, pre-filtered to M365 type (avoids expected 400s from security groups)
Import-Csv .\updatable_details.csv |
    Where-Object { $_.groupType -eq "Microsoft 365" } |
    Export-Csv .\m365_groups.csv -NoTypeInformation

Test-MailboxAccess -Tokens $tokens -InputCsv .\m365_groups.csv -CheckGroupsOnly

# Probe a specific set of groups by GUID
Test-MailboxAccess -Tokens $tokens -GroupIds @("guid1","guid2","guid3") -CheckGroupsOnly

# Resume user probe from position 250
Test-MailboxAccess -Tokens $tokens -UserList .\users.txt -SkipUsers 250

Step 2 — Get-MailboxMessages

Reads or searches messages from accessible mailboxes. By default ingests AccessibleMailboxes.csv from Test-MailboxAccess and filters to rows where Accessible = True. Use -MailboxId to target one or more mailboxes directly by UPN, email address, or GUID without needing a CSV.

Without -SearchTerm, retrieves the top N most recent items per mailbox:

  • Users: GET /users/{id}/mailFolders/Inbox/messages?$top=N
  • Groups: GET /groups/{id}/conversations?$top=N

With -SearchTerm, performs a search:

  • Users: GET /users/{id}/messages?$search="term"&$top=N — server-side OData $search, applied to subject and body. Requires the same Mail.Read.Shared permission. Supports plain keyword terms and phrases; does not support KQL managed properties (filetype:, path:, etc.).
  • Groups: GET /groups/{id}/conversations?$top=N then client-side filter on the topic and preview fields. The Graph REST API does not expose full-text search for group conversations; this is a known limitation of the endpoint. Use -PageResults to fetch all conversation pages before filtering for the most complete results.

-SearchTerm accepts both plain keywords and default_detectors.json KQL query strings. When the search term contains KQL syntax (e.g. (filetype:txt) AND ("AWS_ACCESS_KEY_ID" OR "AWS_SECRET_ACCESS_KEY")), quoted phrases are extracted and used as the actual search terms. filetype:, filename:, NEAR(), and boolean operators are stripped. Plain keyword terms are passed through unchanged. This means the same detector JSON used with Invoke-SearchSharePointByList works here without modification.

Comparison with Invoke-SearchMailbox: GraphRunner's Invoke-SearchMailbox uses the POST /search/query Graph Search API with KQL, which supports richer managed property syntax but is limited to the current user's own mailbox. Get-MailboxMessages with -SearchTerm uses OData $search on the target user's messages endpoint, which works across other users' accessible mailboxes but with simpler query syntax.

Parameters

Parameter Required Default Description
-Tokens Yes Token object from GraphRunner
-InputCsv No AccessibleMailboxes.csv Accessible mailboxes CSV from Test-MailboxAccess. Ignored when -MailboxId is used.
-MailboxId No One or more mailbox identifiers (UPN, email, or GUID) to target directly. Bypasses -InputCsv.
-MailboxType No User Type of the mailboxes supplied via -MailboxId. User or Group.
-Type No Both User, Group, or Both — filter which mailbox types to read from -InputCsv. Ignored when -MailboxId is used.
-MessageCount No 25 Maximum messages to retrieve per mailbox (applies per page when -PageResults is set)
-SearchTerm No Keyword, phrase, or default_detectors.json KQL query string. Triggers search mode. Quoted phrases are extracted from KQL; filetype:, NEAR(), and boolean operators are stripped.
-DetectorName No Custom Label written to the Detector Name column in the output CSV
-OutFile No CSV file to append results to. Results are appended per mailbox as they arrive.
-ReportOnly No switch Suppress console message display
-PageResults No switch Page through all available results via @odata.nextLink
-GraphRun No switch Suppress per-mailbox status output; only print when hits are found. Use in detector loops.
-RefreshInterval No 300 Seconds between proactive token refreshes
-ClientID No Auto-detected Override the client ID used for token refresh

Output CSV columns

Both Get-MailboxMessages and Search-MailboxCache produce the same schema. The three match columns are populated only in search mode; they are empty strings when no -SearchTerm is provided.

Column Description
Detector Name Value of -DetectorName
Mailbox Type User or Group
Mailbox ID UPN (users) or GUID (groups)
Mailbox Display Display name
Mailbox Address UPN or group mail address
Subject Message subject (users) or conversation topic (groups)
Sender Sender email address (users) or sender display names from uniqueSenders joined by , (groups)
Receivers toRecipients addresses joined by , (users only; empty for groups)
Date receivedDateTime (users) or lastDeliveredDateTime (groups)
Preview bodyPreview (users) or preview field (groups shallow path) or first 500 chars of stripped body (groups deep path)
HasAttachments True or False; empty for group shallow path and folder-walk cache mode
AttachmentNames Comma-joined attachment file names from $expand metadata; empty for group shallow path, folder-walk cache mode, and messages with no attachments
BodyFile Absolute path to the saved body file if -SaveTo was used; empty otherwise
Match Location Which fields contained a match: Body, Subject, AttachmentNames, or combinations. Empty when not in search mode.
Match Exact matched substring(s) as they appear in the email, preserving original casing. Comma-joined if multiple distinct terms matched across fields. Empty when not in search mode.
Match Context ±100 characters around the first match in the highest-priority field (Body > Subject > AttachmentNames), with ... at truncation points. For user mailboxes without -SaveTo, falls back to bodyPreview for body context. Empty when not in search mode.

Examples

# Read top 25 messages from all accessible mailboxes (AccessibleMailboxes.csv in current dir)
Get-MailboxMessages -Tokens $tokens

# Explicit CSV path
Get-MailboxMessages -Tokens $tokens -InputCsv '.\audit\AccessibleMailboxes.csv'

# Read top 10, write to file, no console output
Get-MailboxMessages -Tokens $tokens -MessageCount 10 -ReportOnly -OutFile .\messages.csv

# Explicit CSV path, read top 10
Get-MailboxMessages -Tokens $tokens `
    -InputCsv     '.\audit\AccessibleMailboxes.csv' `
    -MessageCount 10 `
    -ReportOnly   -OutFile .\messages.csv

# Read only from group inboxes
Get-MailboxMessages -Tokens $tokens -Type Group -ReportOnly -OutFile .\group_messages.csv

# Search all accessible mailboxes for a term
Get-MailboxMessages -Tokens $tokens -SearchTerm "password" -ReportOnly -OutFile .\hits.csv

# Search with explicit CSV path
Get-MailboxMessages -Tokens $tokens `
    -InputCsv   '.\audit\AccessibleMailboxes.csv' `
    -SearchTerm "password" `
    -ReportOnly -OutFile .\hits.csv

# Search with full pagination (slower but complete results, especially for groups)
Get-MailboxMessages -Tokens $tokens `
    -SearchTerm "confidential" `
    -PageResults -MessageCount 500 `
    -ReportOnly  -OutFile .\hits.csv

# Target a specific user directly -- no CSV required
Get-MailboxMessages -Tokens $tokens -MailboxId "user@contoso.com" -SearchTerm "invoice"

# Target multiple users by GUID
Get-MailboxMessages -Tokens $tokens `
    -MailboxId    "guid1","guid2" `
    -MessageCount 50 -ReportOnly -OutFile .\messages.csv

# Target a specific M365 group inbox with deep fetch
Get-MailboxMessages -Tokens $tokens `
    -MailboxId   "group-guid" -MailboxType Group `
    -DeepGroupSearch -Force `
    -SaveTo .\MailBodies -OutFile .\messages.csv

Note: When using -MailboxId, the Mailbox Display and Mailbox Address columns in the output CSV will echo back the value you provided rather than the resolved display name or address — no extra API call is made to look them up. For UPNs this is fine; for bare GUIDs the columns will show the GUID.


Step 3 — Search-MailboxCache

Searches body files saved by a previous Get-MailboxMessages -SaveTo run. Makes zero API calls and generates no authentication log entries, making it the preferred approach for repeated keyword searches once mail has been downloaded.

Two modes:

  • CSV mode (-InputCsv): Uses the CSV written by Get-MailboxMessages -OutFile as an index. Searches Subject, full body content (via the BodyFile path in the CSV), and the AttachmentNames column. All original metadata — Sender, Receivers, Date, HasAttachments — is preserved in output. This is the richer mode and should be preferred when available.
  • Folder-walk mode (-CachePath only): Walks the SaveTo folder structure directly when no CSV index exists. Searches filename-derived subject and body content. Sender, Receivers, and AttachmentNames are not available in this mode.

When both -InputCsv and -CachePath are supplied, CSV mode takes precedence.

Coverage note: Search-MailboxCache can only find content that was previously downloaded. Unlike Get-MailboxMessages, it does not reach out to Graph API and will miss emails that arrived after the last download run or that were not downloaded because -SaveTo was not used.

Parameters

Parameter Required Default Description
-InputCsv One of CSV written by Get-MailboxMessages -OutFile. Used as the search index.
-CachePath One of Folder written by Get-MailboxMessages -SaveTo. Used for folder-walk mode when no CSV is available.
-SearchTerm Yes Keyword, phrase, or default_detectors.json KQL query string. Quoted phrases are extracted from KQL; other operators are stripped. Checked against Subject, body content, and attachment names.
-OutFile No Path to write matching results as CSV (same schema as Get-MailboxMessages output).
-DetectorName No "Custom" Label for the Detector Name column in output.
-ReportOnly No Suppress per-hit console output.
-GraphRun No Suppress all non-error console output for use in detector loops.

Examples

# CSV mode (preferred): search full body content using the OutFile CSV as index
Search-MailboxCache -InputCsv .\messages.csv `
    -SearchTerm "invoice" `
    -OutFile .\hits.csv

# Folder-walk mode: no CSV available, scan saved body files directly
Search-MailboxCache -CachePath .\MailBodies `
    -SearchTerm "invoice" `
    -OutFile .\hits.csv

# Silent mode for detector loops
Search-MailboxCache -InputCsv .\messages.csv `
    -SearchTerm   "password" `
    -DetectorName "Credential Search" `
    -ReportOnly   -OutFile .\hits.csv -GraphRun

Full Workflow Example

# Load dependencies
. .\GraphRunner\GraphRunner.ps1
. .\Resume-GroupAudit.ps1
. .\Invoke-MailboxAudit.ps1

# Authenticate
Invoke-RefreshGraphTokens -TenantID "contoso.com" `
    -ClientID "04b07795-8ddb-461a-bbee-02f9e1bf7b46" `
    -RefreshToken "0.A..."

# [Optional] Enumerate groups first (if not already done)
Get-GraphGroups -Tokens $tokens -OutputFile .\all_groups.csv
Test-GraphGroupMemberAccess -Tokens $tokens -InputCsv .\all_groups.csv
Get-MemberAccessGroupDetails -Tokens $tokens -InputFile .\updatable_ids.txt `
    -OutputFile .\updatable_details.csv

# Step 1: Probe user and group inboxes
# updatable_details.csv has groupType, so we can pre-filter to M365 groups
Import-Csv .\updatable_details.csv |
    Where-Object { $_.groupType -eq "Microsoft 365" } |
    Export-Csv .\m365_groups.csv -NoTypeInformation

Test-MailboxAccess -Tokens $tokens `
    -UserList  .\users.txt `
    -InputCsv  .\m365_groups.csv `
    -OutputFile .\AccessibleMailboxes.csv

# Step 2: Download all messages and bodies (one-time API run)
Get-MailboxMessages -Tokens $tokens `
    -MessageCount 500 -PageResults `
    -DeepGroupSearch `
    -SaveTo   .\MailBodies `
    -OutFile  .\messages.csv `
    -ReportOnly

# Step 3: Search the cache repeatedly -- zero API calls from here on
# Ad-hoc searches
Search-MailboxCache -InputCsv .\messages.csv -SearchTerm "password"   -OutFile .\hits.csv
Search-MailboxCache -InputCsv .\messages.csv -SearchTerm "invoice"    -OutFile .\hits.csv
Search-MailboxCache -InputCsv .\messages.csv -SearchTerm "vpn"        -OutFile .\hits.csv

# Or run the full detector loop against the cache
$detectors = (Get-Content '.\default_detectors.json' | ConvertFrom-Json).Detectors
foreach ($detect in $detectors) {
    Search-MailboxCache `
        -InputCsv     .\messages.csv `
        -SearchTerm   $detect.SearchQuery `
        -DetectorName $detect.DetectorName `
        -ReportOnly   -OutFile .\hits.csv -GraphRun
}

Detector Loop

Live search (Get-MailboxMessages)

The detector loop pattern from the GraphRunner wiki works with Get-MailboxMessages in place of Invoke-SearchSharePointAndOneDrive. Results from all detectors are appended to a single CSV.

$folderName = "MailboxSearch-" + (Get-Date -Format 'yyyyMMddHHmmss')
New-Item -Path $folderName -ItemType Directory | Out-Null
$outFile    = "$folderName\interesting-mail.csv"
$detectors  = (Get-Content '.\default_detectors.json' | ConvertFrom-Json).Detectors

# Default: looks for AccessibleMailboxes.csv in the current directory
foreach ($detect in $detectors) {
    Get-MailboxMessages -Tokens $tokens `
        -SearchTerm   $detect.SearchQuery `
        -DetectorName $detect.DetectorName `
        -PageResults -MessageCount 500 `
        -ReportOnly  -OutFile $outFile -GraphRun
}

# Explicit path: use -InputCsv if the CSV is elsewhere
foreach ($detect in $detectors) {
    Get-MailboxMessages -Tokens $tokens `
        -InputCsv     '.\audit\AccessibleMailboxes.csv' `
        -SearchTerm   $detect.SearchQuery `
        -DetectorName $detect.DetectorName `
        -PageResults -MessageCount 500 `
        -ReportOnly  -OutFile $outFile -GraphRun
}

The -GraphRun switch suppresses per-mailbox output unless hits are found, keeping the terminal clean during long runs. Results across all detectors accumulate in $outFile.

Note: Because group conversation search is client-side (topic and preview only), some detectors using body-content search terms may produce fewer group hits than equivalent user mailbox hits. For user mailboxes the OData $search is server-side and searches both subject and body. Use -DeepGroupSearch with -SaveTo followed by a Search-MailboxCache loop to get full body coverage for groups.

Cache search (Search-MailboxCache)

If a prior Get-MailboxMessages -SaveTo -OutFile run has already downloaded mail bodies, run the detector loop against the cache instead. This generates zero API calls and no authentication log entries for each subsequent search.

# One-time download (run once per engagement or periodically to refresh)
Get-MailboxMessages -Tokens $tokens `
    -PageResults -MessageCount 500 `
    -DeepGroupSearch `
    -SaveTo  .\MailBodies `
    -OutFile .\messages.csv `
    -ReportOnly

# Detector loop against the cache -- no further API calls
$folderName = "MailboxSearch-" + (Get-Date -Format 'yyyyMMddHHmmss')
New-Item -Path $folderName -ItemType Directory | Out-Null
$outFile    = "$folderName\interesting-mail.csv"
$detectors  = (Get-Content '.\default_detectors.json' | ConvertFrom-Json).Detectors

foreach ($detect in $detectors) {
    Search-MailboxCache `
        -InputCsv     .\messages.csv `
        -SearchTerm   $detect.SearchQuery `
        -DetectorName $detect.DetectorName `
        -ReportOnly   -OutFile $outFile -GraphRun
}

Relationship to GraphRunner

This file supplements, rather than replaces, GraphRunner's mailbox functions. It depends on GraphRunner being loaded and uses Invoke-RefreshGraphTokens from it.

Test-MailboxAccess replaces Invoke-GraphOpenInboxFinder for the user mailbox probe case. The core check is identical — the same GET /users/{id}/mailFolders/Inbox/messages endpoint — but with explicit per-status error reporting, token refresh and retry, group inbox support, resume capability, and a structured CSV output file.

Get-MailboxMessages without -SearchTerm covers the same read case as Get-Inbox, but across multiple accessible mailboxes in bulk. Get-Inbox remains preferable for a targeted single-user read with full body export.

Get-MailboxMessages with -SearchTerm for user mailboxes overlaps in purpose with Invoke-SearchMailbox but differs in scope: Invoke-SearchMailbox uses the Graph Search API (KQL, richer syntax) but only for the current user's own mailbox. Get-MailboxMessages uses OData $search (simpler syntax) but can search across any accessible user or group mailbox.

Search-MailboxCache has no GraphRunner equivalent. It operates entirely offline against the body files and CSV index produced by Get-MailboxMessages -SaveTo -OutFile, generating no API calls or authentication log entries. It is the preferred approach for repeated keyword searches once mail has been downloaded, and provides full body content coverage for group mailboxes where live search is limited to topic and preview.

GraphRunner is authored by Beau Bullock (@dafthack) and licensed under MIT.

Clone this wiki locally