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.