6/25/2025 Important update regarding the cloud hosted Mailbox External Recipient Rate Limit: In order to reduce the impact on customers, Microsoft has again decided to delay the rollout of the cloud ...
In case anyone wants to use Advanced Hunting to get a quick peak, it's fairly decent in comparison with Message Trace. Not vouching for any level of perfection etc., but you can easily get an instant glance at the last 24 hours like this:
let acceptedDomains = datatable (domain: string) [@'AcceptedDomain1.ca',@'AcceptedDomain2.ca'];
EmailEvents
| where InternetMessageId endswith "PROD.OUTLOOK.COM>"
| where EmailDirection =~ 'Outbound' and SenderFromDomain in~ (acceptedDomains)
| summarize count() by SenderFromAddress
By filtering for the InternetMessageId ending with prod.outlook.com, we filter out all the on-premises SMTP traffic that is coming up to EXO/EOP and out to external recipients, ensuring we're looking at only EXO-mailbox-generated emails ("ensuring" may be less than perfect).
If you want to look at the last number of days, summarize the daily stats using bin():
let acceptedDomains = datatable (domain: string) [@'AcceptedDomain1.ca',@'AcceptedDomain2.ca'];
EmailEvents
| where TimeGenerated >= ago(3.75d)
| where InternetMessageId endswith "PROD.OUTLOOK.COM>"
| where EmailDirection =~ 'Outbound' and SenderFromDomain in~ (acceptedDomains)
| summarize count() by SenderFromAddress, bin(Timestamp, 1d)
Note, I could only get 3.75 days' worth to remain inside the 30,000, results cap in Advanced Hunting.
My next idea is to save as a Custom Detection Rule so that we can have alerts if/when 2000/day is exceeded. Then we could just watch for and make note of the alerts/incidents that arise going forward. To make it compatible for Custom Detection Rule, we need Timestamp and ReportId, as well as an entity-identifying property (SenderFromAddress works in this case). For ReportId, I'm just faking it to make the system let me save this as a Custom Detection Rule:
let acceptedDomains = datatable (domain: string) [@'AcceptedDomain1.ca',@'AcceptedDomain2.ca'];
EmailEvents
| where TimeGenerated >= ago(1d)
| where InternetMessageId endswith "PROD.OUTLOOK.COM>"
| where EmailDirection =~ 'Outbound' and SenderFromDomain in~ (acceptedDomains)
| summarize MsgsLast24H = count() by SenderFromAddress, bin(Timestamp, 1d), ReportId = 'placeholder'
| where MsgsLast24H >= 2000
^^ This KQL query above will save as a Custom Detection Rule successfully. Here's a sample of what my rule looks like, as I created on the fly while making this post:
Thanks for sharing. I took a very similar approach when I was looking at this a couple weeks ago, glad I'm on the same page as someone else. That bin function is nice as well - I had done something similar more manually a while back using the `format_datetime()` w/ `extend` and doing my own aggregate based on that.
hi JeremyTBradshaw - Did that work for you? I ended up trying more or less the same thing with a new outbound spam policy using the RecipientLimitExternalPerHour, but apparently there is no enable paramater for the new/set-HostedOutboundSpamFilterPolicy.
When you save a new rule, it runs and checks for matches from the past 30 days of data. The rule then runs again at fixed intervals, applying a lookback duration based on the frequency you choose:
Every 24 hours — runs every 24 hours, checking data from the past 30 days
Every 12 hours — runs every 12 hours, checking data from the past 48 hours
Every 3 hours — runs every 3 hours, checking data from the past 12 hours
Every hour — runs hourly, checking data from the past 4 hours
Continuous (NRT) — runs continuously, checking data from events as they're collected and processed in near real-time (NRT), see Continuous (NRT) frequency
Tip
Match the time filters in your query with the lookback duration. Results outside of the lookback duration are ignored.
So based on that the CDR frequency would need to be "Every 12 hours" or "Every 24 hours" in order to properly parse results from entire 24-hour periods. My screenshot above showed "Every hour" which would not work, so it was a bad example.
I didn't think about outbound spam filter policies for this task, so had better not comment on that part.