Easily tell which transport rules a message triggered

Hello fellow traveller, come sit with me by the fire, there is plenty of room. You look to be weak from your travels, I have plenty of rations that I will gladly share with you. I have many stories from my own travels that I would love to share. Wait…, you want to know about tracking which rules triggered on a message in Exchange Online Protection? Are you sure? Well, since you seem so interested I can certainly share this tale with you as well. Let’s begin.


Always add the action “set the message header to this value” in all EOP transport rules

I’m going to cut right to the chase here and divulge this best practice first. From there I will go into some examples which shows why this is such a great practice.

As the sub title says, always add the action, “set the message header to this value,” for each of your EOP transport rules. This would be added in addition to whatever action you want the rule to take. Here’s an example of what a safe list rule would look like.

This simple addition to your rules will make message tracing and troubleshooting much easier. If an end user forwards you a message for analysis, you no longer need to run a message trace to see what EOP rules triggered as this can now been seen in the message headers.

This will especially come in handy if you are looking at a message that is older than 7 days. In the EOP message trace, to view messages older than 7 days you need to run an Extended Message Trace which can take up to an hour to complete, and if you mistyped any search criteria then you’ll need to wait another hour after you have re-run it.

You will first want to come up with a standard format for your header addition. In my case I use X-ContosoRule-<RuleName> and then I always follow it with the text Rule triggered. The reason I don’t use the same X-header, like X-ContosoRule, for each rule is that EOP rules that trigger will keep overwriting this header so you will only ever see it stamped once in your email headers. This is why the X-header name needs to be unique.

Let’s take a look at some real world examples where this practice can save valuable troubleshooting time.

Example 1 – Email has been safe listed

An end user has received a message in their inbox that they consider spam and forward it to their email admin (that’s you) for investigation. Being the great Sherlock Holms admin that you are, you load up the headers and jump to the X-Forefront-Antispam-Report entry to see how EOP classified it. Here’s what we see.


Do you see anything suspicious (hint: I highlighted them)? How about SFV:SKN (means the message was marked as safe prior to the content filter) and SCL:-1 (the content filter was bypassed). This message was safe listed and so it skipped the EOP content filter. To tell if a transport rule did the safe listing we could run a message trace, but that is time consuming and not as slick as this best practice. Since we already have the headers open, let’s look for the header that each of our transport rules will add, X-ContosoRule-<RuleName>. Sure enough, we see an entry in our message headers.

X-ContosoRule-SafelistByDomain: Rule triggered

We now know that only one of our transport rules triggered on this message and because of our great naming convention we can see that this was the reason that the message was safe listed. In this situation a domain may have been added to this rule by accident.

If you had run a message trace we would have seen evidence of the rule that fired as well.

Example 2 – Email ends up in junk mail folder

You have safe listed mail from a particular domain, but end users are reporting that some mail from this domain is still ending up in their junk mail folder. Again, you obtain a copy of the received message from a user’s junk mail folder and look at the header. Being the rock star that we are, we jump to the X-Forefront-Antispam-Report header and find the following.


SVF:SKS indicates that this message was marked as spam before the content filter (ie. By a transport rule). We can also see that the message was given an SCL of 9. Now let’s look for our rule headers, X-ContosoRule-<rulename> , to see which rules triggered. Here’s what we find.

X-ContosoRule-SafeDomainFound: Rule triggered
X-ContosoRule-ExecutableFound: Rule triggered

We can see two rules triggered here. What happened was that the first rule triggered which gave this message an SCL of -1. But then a second rule triggered based on executable content being found in the message which then changed the SCL to 9. Remember that rules execute chronologically starting with the rule that has a priority of 0, followed by priority 1, then priority 2, etc.

Again we could have run a message trace which would have shown us the two rules that triggered.

Example 3 – Inbound message has landed in the quarantine

Lastly, here we have an inbound message that has landed in the EOP quarantine. If you have ever looked at the headers for a message in the quarantine you will know that they contain a lot of information! Typically this information is stripped before the message lands in the inbox, but when the message is in the quarantine these extra headers are still present in all their glory. More on that in a few paragraphs. Let’s first pull the headers from this message in the quarantine.

Because this message is in the quarantine I’m not going to look at the X-Forefront-Antispam-Report header. Instead I’m going to jump right to my transport rule headers. Here’s what we find.

X-ContosoRule-SafeDomainFound: Rule triggered
X-ContosoRule-ExecutableFound: Rule triggered
X-ContosoRule-BlockedSender: Rule triggered

Three rules triggered on this message. Being the admin, I know that the rule BlockedSender has an action of “move to quarantine,” and this is why the message wound up there.

I had mentioned above that headers obtained from messages in the quarantine contain a lot of extra information that you normally can’t see on a message that has arrived in a user’s mailbox. One extra header that you will see in this situation is X-MS-Exchange-Organization-Rules-Execution-History. As I’m sure you can guess, this will tell us all rules that triggered on a message. This header is stripped on messages before they are delivered to mailboxes and so you can only see this for messages in the quarantine. Here’s what this section shows for this example. I have highlighted the actual names of the rules.

 TransportVersioned.Sender domain is tailspintoys.com%%%3435319c-655a-49fb-81da-960aafb42a67%%%
 TransportVersioned.Executable content found%%%14c7ff43-f182-4653-942a-638de95d7222%%%
 TransportVersioned.Move to quarantine%%%131982ac-07dc-4a0f-a1b9-d59b2e60c40a%%%

Note that here we are also seeing the GUIDs of each of the rules. These can be obtained for your rules in PowerShell.

One last note about messages that have been redirected from the quarantine. I ran a message trace on this message and see the following.

Notice anything strange? How about that it does not show any rules that triggered on this message. This is expected and a “special case.” If a rule triggers with the action “redirect to quarantine,” you will not see evidence of ANY rules that have triggered on this message in the message trace (however an extended message trace will still show the triggered rules). This is why the headers in the quarantine can be especially useful for figuring out what happened, and if all of our rules add an X-header than it will be very easy to quickly see what was triggered on a message.


Final thoughts

I hope I was able to show you the value behind the best practice of having all your rules add an x-header when they trigger. Also, after my short last post I had to make it up with a long post. I hope you made it to the end.


Anti-spam message headers


P.S. There are some posts with questions in the comments that I need to reply to. I will try to get to those in the next couple of weeks.