Message Trace, the PowerShell Way

From my experience, a very small number of people actually choose PowerShell over the GUI (Graphical User Interface, ie. The Office 365 Portal). But once you get a grasp of PowerShell and write some scripts, you’ll see the light and going back to your old ways will be very hard. PowerShell has two big advantages. First, once you are proficient you’ll be able to pull and modify data much faster with PowerShell than going into the portal. Second, most managers aren’t PowerShell experts, so when you are working and have your PowerShell window open they will be extra impressed with you! Don’t worry, you can thank me later.

In this article I’m going to focus on using PowerShell to obtain message trace results from the past 48 hours. The commands in this article will work for date ranges up to seven days in the past. For messages older than seven days we need to run an extended message trace, or Start-HistoricalSearch.

Typical message trace run

Let’s start by running a typical message trace in EOP which returns all results for the past 48 hours.

In this view we can sort based on the columns presented to us and can double click for additional information. Now let’s look at how PowerShell can improve upon this experience.

Message trace using PowerShell

You’ll see below that I’m going to be piping my results to Out-GridView. If you have not used this cmdlet before, it essentially will launch a new interactive window where you can view the results. Let’s take a look.

The following script will return the message trace results of the last 48 hours.

$dateEnd = get-date
$dateStart = $dateEnd.AddHours(-48)

Get-MessageTrace -StartDate $dateStart -EndDate $dateEnd | Select-Object Received, SenderAddress, RecipientAddress, Subject, Status, ToIP, FromIP, Size, MessageID, MessageTraceID | Out-GridView 

Here is what the results look like.

Just like with the portal message trace, we can sort by clicking the column headers. Moving onto the differences, the first difference you’ll notice here is that there are more columns presented to us, and even nicer is that we can sort and filter on these extra columns which is something you can’t do in the portal message trace. For example, in the above screenshot, I have set a filter to only show me the messages that were larger than 12 MB, something that is not possible with the portal based message trace. Yes… now we are seeing some of the power!


Next let’s look for messages from the past 48 hours that have a Status of “Failed.” Here is our script.

$dateEnd = get-date
$dateStart = $dateEnd.AddHours(-48)

Get-MessageTrace -StartDate $dateStart -EndDate $dateEnd | Select-Object Received, SenderAddress, RecipientAddress, Subject, Status, ToIP, FromIP, Size, MessageID, MessageTraceID | Where {$_.Status -eq "Failed"} | Out-GridView

This shows that we have five failed messages from the last 48 hours. Now let’s pipe our results to Get-MessageTraceDetail to find out more information on the failed messages. Here’s our next script to do this.

$dateEnd = get-date
$dateStart = $dateEnd.AddHours(-48)

Get-MessageTrace -StartDate $dateStart -EndDate $dateEnd | Where {$_.Status -eq "Failed"} | Get-MessageTraceDetail | Select-Object MessageID, Date, Event, Action, Detail, Data | Out-GridView

And this will return the following:

Here I can see the status of those failed messages. “Hop count exceeded – possible mail loop.” In this situation, a message was destined for a mailbox that didn’t exist in the cloud, because of a connector problem the message kept looping around Exchange Online before it was finally rejected.

Also notice the Data column above. Here we can see a lot of extra information that is not present in the portal message trace.

<root><MEP Name="ConnectorId" String="To_DefaultOpportunisticTLS"/><MEP Name="DeliveryPriority" String="Normal"/><MEP Name="OutboundProxyTargetIPAddress" String="207.46.XXX.XXX"/><MEP Name="OutboundProxyTargetHostName" String=""/><MEP Name="RecipientStatus" String="[{LRT=};{LED=554 5.4.6 Hop count exceeded - possible mail loop};{FQDN=};{IP=}]"/></root>

What’s highlighted in yellow is all the portal message trace will show. The rest of the data can only be obtained with PowerShell, or through an Extended Message Trace. We can see the IP that issued the 500 error, the connector that was used, and even the MX record where Exchange Online was trying to deliver the message to.


Let’s take a look at another example. This time we want to track a particular message based on its Message ID. An end user told us that a sent message resulted in an NDR so we already know it failed. The NDR provides us with the MessageID. Let’s run the following to get directly to the message trace details for this particular message.

Get-MessageTrace -MessageId | Get-MessageTraceDetail | Select MessageID, Date, Event, Action, Detail, Data | Out-GridView

Right away we can see the error “Unable to relay.” We can also see this message was tried to be delivered using TLS. What we are seeing here is most likely a connector problem.

Of course, you don’t have to use Out-GridView, I just find it sometimes handy for quick filtering of the results.


Lastly, the searches I have provided above are fairly open ended. The tenant I used has a very low number of message coming through it. If you are finding that Get-MessageTrace is taking too long to run or timing out, I would suggest making your search stricter. For example, here is how you would search for messages coming from between a particular time range.

Get-MessageTrace -SenderAddress -StartDate “12/15/2014 21:00” -EndDate “12/15/2014 22:00”

Note that the time specified must be in UTC.

Other Benefits

PowerShell can be set to run automatically. One example would be to create a script which checks for messages with a status of “Failed.” Have a server run this script every hour and if you see a large number of results, have the script or the Scheduled Task send you an alert.


For those that have never used PowerShell before it can look a little scary. There are a lot of great tutorials online to get you started and once you get your feet wet you’ll start moving fast. Script once, run many times.

On the personal side, I have a PowerShell script to automatically log me into my tenant. Once connected, I have scripts that pull Transport Rules, Message Trace results, Connector information, and much more. This is often faster than logging directly into the portal.

Finally, I'd like to thank Brian, one of my customers, who gave me the idea for this post.

Happy scripting!


Connect to Exchange Online using remote PowerShell
Exchange Online cmdlets

Connect to Exchange Online Protection using remote PowerShell
Exchange Online Protection cmdlets

Run a Message Trace and View Results
Get-MessageTrace cmdlet
Get-MessageTraceDetail cmdlet