Exchange Online: Generate a Mail Read/Unread Report Using Message Trace V2 and Microsoft Graph

Every so often, someone asks a deceptively simple question about email:

“Can we get a report showing who actually read a specific message?”

Exchange Online doesn’t provide a neat, built-in “read analytics” report for arbitrary messages (and even if it did, email clients have their own opinions about when a message is “read”). Still, if you’re operating inside a Microsoft 365 tenant and the message landed in Exchange Online mailboxes, you can build a report that’s accurate enough to be genuinely useful.

The trick is to combine two sources of truth:

  • Message Trace V2 tells you who actually received the message (Delivered).
  • Microsoft Graph lets you query each recipient mailbox for the message’s internetMessageId and check the IsRead flag.

That’s exactly what my script does – it avoids scanning the entire tenant and only checks recipients confirmed by trace.

Why this approach works

If you try to answer “who read this email?” by scanning every mailbox, you’re going to have a bad time – especially in large tenants.

This script flips the problem around:

  1. Start with delivery reality (Message Trace V2, Delivered)
  2. Then ask only those recipients whether the message is read (Graph, IsRead)

In other words: filter hard first, query second.

A practical use case

A common scenario is an internal broadcast email – for example, an internal newsletter sent by your marketing team. They don’t need pixel tracking; they just want a simple read/unread breakdown for the message they sent to employees.

With this approach you can provide:

  • delivered recipients (from Exchange trace)
  • read/unread status (from Graph)
  • CSV output for sharing or follow-up reporting

What the script actually does

Step 1 – Message Trace V2: Get delivered recipients

The script runs Get-MessageTraceV2 with:

  • -Status Delivered
  • -SenderAddress <sender>
  • a stable time window:

It then selects the relevant fields:

  • RecipientAddress
  • MessageId (this is the internetMessageId)
  • Subject
  • Received

And de-duplicates recipient/message pairs.

Step 2 – Microsoft Graph: Check IsRead using internetMessageId

For each recipient from trace, the script calls Graph and filters the mailbox messages by:

  • internetMessageId eq '<MessageId from trace>'

It returns the newest matching item and records:

  • IsRead
  • subject
  • received timestamp
  • status (OK, NotFoundInMailbox, Error: …)

Requirements

PowerShell Modules

  • ExchangeOnlineManagement
  • Microsoft.Graph

Authentication model

The script uses app-only certificate-based authentication for both:

  • Exchange Online PowerShell (Connect-ExchangeOnline)
  • Microsoft Graph (Connect-MgGraph)

Permissions (high level)

Exchange Online (app-only):

  • must be able to run Get-MessageTraceV2
  • must be able to validate sender via Get-EXORecipient

Microsoft Graph (app-only):

  • Mail.Read (Application) with admin consent

As always: treat application permissions and certificates as privileged assets and govern accordingly.

Running the script

Basic run (console output)

Export detailed + summary CSV reports

This will create two csv files to use for reporting:

CSV

Summary-only mode

Understanding the output

The summary provides:

  • RecipientsFound
  • Read
  • Unread
  • NotFoundInMailbox
  • Errors
  • ReadPercentage

The detailed CSV includes per-recipient rows with:

  • recipient address
  • message id (internetMessageId)
  • received timestamp
  • isRead
  • status

This is typically everything people want and a bit more than they expected.

Important notes

IsRead is not an audit log

The IsRead flag is mailbox state, influenced by clients and settings (preview pane, mobile behavior, cached mode, etc.). It’s a strong indicator, but not a courtroom exhibit.

Delivered ≠ Read

Message trace confirms delivery. “Read” requires the Graph step and even then, it reflects mailbox state rather than user intent.

Throttling can happen

If you query a large number of recipients, Graph may throttle. The script includes -ThrottleMs so you can slow down calls when needed.

Script Source

Complete script as always is available for download on Azure365Addict GitHub.
Feel free to customize the script to fit your specific needs and improve your Exchange Online hygiene.

If you have any questions or want to extend this into automated cleanup or reporting, feel free to reach out.

Happy scripting!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top