Message trace remains one of the most useful tools in the hands on Exchange Online administrators, as it helps troubleshoot a range of issues, and can be used to build some more detailed reports than what’s available within the service. At the end of last year, Microsoft announced a preview of a “new” message trace experience, and last month, it was released to GA status. I took this opportunity to update the good old Get-DetailedMessageStats.ps1 script… but first let’s talk about what the changes are in the new experience.
Let’s start with the good news. The new message trace ensures feature parity with the “old” experience, while bringing some nice improvements. The most significant of these is the support for querying data up to 90 days in the past in a synchronous manner, whereas we were previously limited to just a handful of days, and had to run async queries. Up to 10 days of data is available for a single query, but this should not be a problem as in any tenant of meaningful size, you will hit the “page” limit early on and will have to issue additional queries anyway.
Another nice improvement is the larger set of filters supported for the Get-MessageTraceV2 cmdlet, most notably the ability to filter based on a message’s subject, made possible thanks to the -Subject parameter. Do not say that Microsoft does not listen to feedback… even if it takes them decade to deliver! On the same note, we can now also filter based on the message status, i.e. run a query for only messages that were quarantined.
On the negative side of things, Microsoft changed the way “pagination” works, and the new experience is mildly annoying at best. It could have been implemented better IMO, or at the very least align with the pagination experience in the Graph API. Speaking of which, the next negative is the lack of support for Graph API endpoints/methods. One can argue that we do have a suitable replacement on the Graph via the analyzedEmails endpoint, but as we discussed in our article on said endpoint, it’s use case is different.
Which brings us to the next bad news, namely the incoming deprecation of the old experience. Microsoft is giving customers until September 1st to move away from the Get-MessageTrace/Get-MessageTraceDetail cmdlets. The same deadline applies to the MessageTrace report in the good old reporting web service, which is the only supported RESTful interface to query the message trace data programmatically. No alternative is provided at this point, thus any customers and ISVs that still rely on the reporting web service need to move to using PowerShell instead, which might be an issue. The same can be safe in regard to the new throttling guidance, namely 100 requests per 5 minutes.
Now that I’ve given you a crash course on the new message trace experience, let’s see it in action. And what better way to do that than take one of the most popular scripts from days past, and update it to leverage the Get-MessageTraceV2 cmdlet. I’m indeed talking about Alan’s Office 365 Mail Traffic Statistics by User script! In a nutshell, the script is a proof of concept that demonstrates how to fetch message trace data and use it to provide additional statistics per user, such as the number or size of the messages sent and received by them.
Of course, a lot of time has passed since the initial version of the script was released, so it requires some additional changes, apart from switching to the Get-MessageTraceV2 cmdlet. Authentication is one such example, and as customary for my ExO scripts, I try not to tie you to using a specific connectivity method. The script will try to detect and reuse any existing session, and if none was found will prompt you to connect interactively. Should you want to run the script in an automated manner, make sure to add your own connectivity block. Of course, also make sure to address the permission requirements – sufficient permissions in order to run Get-EXORecipient and Get-MessageTraceV2 are required.
After connecting, the script will fetch all recipient objects within the tenant and populates a hashtable using their primary SMTP address value as key. The original code did not account for any messages sent to secondary aliases, nor plus addresses for that matter. In addition, sending from additional addresses was not possible back when the script was originally written. The code has been improved to account for these scenarios, although you might end up with some oddities if a plus address has been stamped on the recipient object itself. A better approach would be to exclude all entries with status of Resolved, but it seems the -Status filter parameter does not accept this as valid value, so such filtering must be done client side.
The biggest change in the script’s logic is in how it handles pagination. I’ve opted for an approach that relies on the presence of the “hint” returned by the service, which unfortunately is implemented via the warning stream. In effect, we suppress the warning while making sure its content is stored in a variable and then processed to extract the “next page” cmdlet. I’m not a big fan of this implementation, as you can see from my comments under the original blog article. The alternative is to create the “next page” syntax yourself, by copying the properties of the last returned entry, like Tony does in his sample script.
Another downside of the new “no pagination” approach is that we cannot have a proper progress indicator, so instead I have added a “poor man’s” variation of it, just so you know whether the script is progressing. Once we fetch the available message trace data, we largely follow the logic of the original script and prepare a hashtable for each recipient, holding the count and size of both inbound and outbound messages, per day. Lastly, we transform the output and dump it into a CSV file in the working directory. And since HTML output seems to be all the rage currently, I’ve asked Copilot to generate the corresponding code. Can I play with the cool kids now? 🙂
To run the script, first get your copy from my GitHub repo. Connect to Exchange Online with your preferred method, or just update the connectivity check in the script (lines 6-10), then invoke the script. As I’ve kept most of the original logic intact, no parameters are needed/available, although there are certainly some use cases that warrant such – for example filtering based on the status, configuring a different date range, specifying the output format or location for saving the files. As always, feel free to adjust the code to best suit your needs. Here’s how to invoke the script:
. .\Get-DetailedMessageStatsV2.ps1
Here is also a sample of how the output looks like in my tenant. The first screenshot shows the generated CSV file, with filter set to show any users with outbound messages. Note the Gmail entry therein – it appears in the output because of a matching mail user object in the tenant. If you don’t want to see such entries, you can adjust the script to fetch only the desired type of recipient objects, i.e. user/shared mailboxes and groups. The second screenshot showcases the HTML output in the form of a sortable table.


And with that, we can close this article. We’ve taken the new Get-MessageTraceV2 cmdlet and put it to the test for one of the common scenarios, while at the same time giving new life to one of all time’s favorite community-created PowerShell scripts. Keep in mind that the updated script is still just a “proof of concept” and not something you should run in your production tenant. It requires additional work, especially if you want to cover the full range of message trace data available. If you want to run the script in unattended manner, make sure the user/service principal you are using has sufficient permissions to run the Get-MessageTraceV2 cmdlet!