Threat Hunting via InternetMessageId (+ KQL Queries)

Welcome again!

First of all, I would like to share that I really enjoyed writing this article because everything you will read has been new for me, so I was learning by doing, which is my favourite way to improve my knowledge. In this case, I want to talk about applying Threat Hunting to the email field InternetMessageId, which is a default value that all emails contain.

After reading about the format and content of InternetMessageId field, I personally feel that we are using the field itself as a whole value for threat hunting, but we are not going further by analyzing and extracting its content.

That is what I am trying to do in this article, and I feel that I am just discovering a small percentage of all the types of threat detections that we could create based on it.

Years ago, it was easier to identify a common pattern in the format of the InternetMessageId, which used to consist of a sender-generated local part and a domain, where the domain indicates the system that generated the message and the local part may contain timestamps, process identifiers, or internal routing information depending on the email platform. However, nowadays we have different formats. One of the common cases that is still active is having this value include relevant information such as date, time, process ID, and others. If you want to know more about it and potential use cases for threat hunting or forensics, I totally recommend the following document: https://ro.ecu.edu.au/cgi/viewcontent.cgi?article=1048&context=adf

Email ‘Message-IDs’ helpfur for forensic analysis? — Satheesaan Pasupatheeswaran Edith

InternetMessageID Threat Hypotesis

One of the key aspects when doing threat hunting is to create hypotheses based on, for example, a specific threat. In this case, I was wondering if, with the InternetMessageId — which is a mandatory field in all emails that we receive or send — I could extract something useful. So I started to create different hypotheses; some of them only gave me insights, while others provided what I needed to create detection rules, helping to identify possible attacks such as phishing, MFA theft via email codes, and others.

(Hypothesis 1) — Classifying messageID by it longitude (Information)

In the first scenario, I was wondering if, based on the number of characters of each InternetMessageId, I would be able to detect some pattern and honestly, just information. Based on the ThreatType field, I classified the emails with and without ThreatTypes (Spam,Phish, Malware), and as you can see in the graph below, threats tend to appear in emails between 50 and 75 characters from my side, but also the legitimate ones. So, good to know, but not something to define as a rule to detect threats.

//Sergio Albea  19-04-2026 ©️
EmailEvents
| where isnotempty(InternetMessageId)
| extend CleanId=trim(@“<>”, InternetMessageId)
| extend LocalPart=split(CleanId,“@”)[0]
| extend LocalLength=strlen(LocalPart)
| extend LengthGroup=case(LocalLength < 10, “Less than 10”, LocalLength < 50, “Less than 50”, LocalLength < 75, “Less than 75”, LocalLength < 100, “Less than 100”, LocalLength < 200, “Less than 200”, “Others”)
| extend ThreatStatus=case(isnotempty(ThreatTypes) and ThreatTypes !~ “”, “Threats”, “NoThreats”)
| summarize Count=count() by LengthGroup,ThreatStatus
| evaluate pivot(ThreatStatus,sum(Count))
| extend Threats=coalesce(Threats,0),NoThreats=coalesce(NoThreats,0)
| extend SortOrder=case(LengthGroup == “Less than 10”, 1, LengthGroup == “Less than 50”, 2, LengthGroup == “Less than 75”, 3, LengthGroup == “Less than 100”, 4, LengthGroup == “Less than 200”, 5, 6)
| order by SortOrder asc
| project LengthGroup,NoThreats,Threats
| render columnchart

(Hypothesis 2) — Based on enclosed strings (Information-awareness)

“Defined in RFC 5322, it is optional but recommended, consisting of a string enclosed in angle brackets (< and >), with a syntax of msg-id = [CFWS] “<” id-left “@” id-right “>” [CFWS], where id-left and id-right are typically dot-atom-text resembling an email address local part and domain, respectively, to ensure uniqueness across systems.[1] The identifier is generated by the originating host and must not be reused for other messages to avoid conflicts in email processing.” [1] Message-ID (Grokipedia.com)

This second hypothesis is identifying me some kind of initiative, projects or non-popular services, and some vendor notifications sending spam. It can help to contact some of your partners if they are not following the mentioned best practice to ensure uniqueness across systems and potential Spam sources. However, I have not observe it as a pattern to send malicious content.

//Sergio Albea  19-04-2026 ©️
EmailEvents
| where InternetMessageId !startswith “<” or InternetMessageId !endswith “>”
| join kind=inner EmailUrlInfo on NetworkMessageId
| summarize make_set(Subject),make_set(SenderIPv4), dcount(InternetMessageId),make_set(Url) by SenderFromDomain, ThreatTypes

(Hypothesis 3) — Based on Microsoft Activities ( Threat Detection)

I have not been able to find oficial documentation but there are good indirect references that confirm it.

Seems Microsoft is sending notification about files shared, One Time code and other link to contribute to files, are using a notification system identified as odspnotify. You can use the execute the folowing KQL to get a global view which also take the InternetMessageID values to know which type of activities are related to the email shared.
//Sergio Albea  19-04-2026 ©️
EmailEvents
| where isnotempty(InternetMessageId) and InternetMessageId contains “odspnotify”
| where SenderFromDomain !in (“sharepointonline.com”,“trustedomain2.ch”)
| extend IMF = trim(@“[<>]”, tostring(InternetMessageId))
| extend IMF_Left = tostring(split(IMF, “@”)[0])
| extend IMF_Parts = split(IMF_Left, “;”)
| extend MessageType = tostring(IMF_Parts[0])
| extend RecipientCorrelationId = tostring(IMF_Parts[1])
| extend ProcessingStage = tostring(IMF_Parts[2])
| extend RetryInfo = tostring(IMF_Parts[3])
| extend SendEmailInfo = tostring(IMF_Parts[4])
| extend SendEmailParts = split(SendEmailInfo, “-”)
| extend SendAction = tostring(SendEmailParts[0])
| extend IOE = tostring(SendEmailParts[1])
| extend TenantId = tostring(SendEmailParts[2])
| extend RoutingHint = tostring(SendEmailParts[3])
| extend ActivityId = strcat_array(array_slice(SendEmailParts, 4, array_length(SendEmailParts) - 1), “-”)
| extend MessageTypeFull = tostring(IMF_Parts[0])
| extend MessageTypeName = extract(@“^(.)-[0-9a-fA-F-]{36}$“, 1, MessageTypeFull)
| extend MessageTypeGuid = extract(@”([0-9a-fA-F-]{36})$“, 1, MessageTypeFull)
| extend MessageTypeAfterDash = tostring(split(MessageTypeFull, “-”)[1])| join kind=inner EmailUrlInfo on NetworkMessageId
| summarize Urls=make_set(Url) by Subject, SenderDisplayName, SenderMailFromAddress, SenderFromDomain, SenderIP=iff(isnotempty(SenderIPv4),SenderIPv4,SenderIPv6), ThreatTypes,MessageTypeName,RecipientDomain, RecipientEmailAddress

(Hypothesis 3.1) — Based on Microsoft OneTimePasscode Notification ( Threat Detection)

If you are in a country where there is more than one local language, like me in Switzerland, it is challenging to create detections based on subjects because you can have them in multiple languages. Therefore, focusing on the value odspnotify inside of InternetMessageId — which does not change based on the language — helps to detect cases.

This one is related to OneTimePassCode and is useful to identify if the code is sent to other domains, which could mean that a forwarding rule has been configured or some other suspicious activity.

//Sergio Albea  19-04-2026 ©️
EmailEvents
| where InternetMessageId contains “OneTimePasscode”
| where RecipientDomain !in (‘trustedomain.ch’,‘trustedomain2.ch’)
| project InternetMessageId,Subject, RecipientDomain, RecipientEmailAddress, ReportId

(Hypothesis 3.2) — Based on Shared File Links MEssage ( Threat Detection)

I love this one. I don’t know how many time I suffered malicious actors sharing links via Microsoft services such as SharePoint or other sites manipulating them for phishing, malware, or other malicious purposes.

I have seen multiple detections to detect phishing campaigns based on subjects, such as detecting when an email subject ends with “with you” or contains “shared.” However, if the original mailbox is configured in another language, these rules become useless.

This changes when using the same approach as before: specifying the notification system odspnotify and, in addition, specifying the first part of the InternetMessageId, which tells us the action requested or performed by the email — in this case, share.

For this detection, I recommend adding your trusted domains, but it is incredibly useful to detect when links from Microsoft systems are shared with your users from unexpected domains.

//Sergio Albea  19-04-2026 ©️
EmailEvents
| where InternetMessageId contains “odspnotify”
| where SenderFromDomain !in (“sharepointonline.com”,“trustedomain2.ch”)
| extend IMF = trim(@”[<>]“, tostring(InternetMessageId))
| extend IMF_Left = tostring(split(IMF, “@”)[0])
| extend IMF_Parts = split(IMF_Left, “;”)
| extend MessageTypeFull = tostring(IMF_Parts[0])
| extend MessageTypeName = extract(@”^(.
)-[0-9a-fA-F-]{36}$”, 1, MessageTypeFull)
| extend MessageTypeGuid = extract(@“([0-9a-fA-F-]{36})$”, 1, MessageTypeFull)
| where MessageTypeName has ‘Share’
| extend MessageTypeAfterDash = tostring(split(MessageTypeFull, “-”)[1])| join kind=inner EmailUrlInfo on NetworkMessageId
| summarize Urls=make_set(Url) by InternetMessageId,Subject, SenderDisplayName, SenderMailFromAddress, SenderFromDomain, SenderIP=iff(isnotempty(SenderIPv4),SenderIPv4,SenderIPv6), ThreatTypes,MessageTypeName,RecipientDomain, RecipientEmailAddress, ReportId

Hypothesis 4 — Based on Dates (Threat Detection)

The are some cases where InternetMessageIds contain a date integrated, sometimes encoded, sometimes not. Therefore, we have two timestamps to compare:

InternetMessageId date → usually when the sending system generated the message Timestamp (EmailEvents) → when Microsoft received/processed the email

So if we compare “when it was created” vs “when it arrived”, we can detect cases such as emails created by the system but, due to local configuration, email campaigns that deliver slowly, or messages that were postponed compared to when they were received by Microsoft.

The ratio of true positive was really high and multiple emails were not detected as a Threat so good case to add as a detection.

//Sergio Albea  19-04-2026 ©️
EmailEvents
| extend Timestamp_YYYYMMDD = format_datetime(Timestamp, “yyyyMMdd”)
| extend Timestamp_IM = format_datetime(Timestamp, “yyyyMM”)
| extend IMF = tostring(InternetMessageId)
| where IMF contains Timestamp_IM
| extend StartPos = indexof(IMF, Timestamp_IM)
| extend Extracted_IMF_Date = iff(StartPos >= 0 and strlen(IMF) >= StartPos + 8, substring(IMF, StartPos, 8), “”)
| extend Extracted_IMF_Date_dt = todatetime(strcat(substring(Extracted_IMF_Date,0,4), “-”, substring(Extracted_IMF_Date,4,2), “-”, substring(Extracted_IMF_Date,6,2)))
| where Timestamp > Extracted_IMF_Date_dt
| extend case1 = iff(IMF contains Timestamp_YYYYMMDD, “valid”, “other”)
| join kind=inner EmailUrlInfo on NetworkMessageId
// comment the line below if you want to see all InternetMessageId with date populated and the cases detected as Threat
| where case1 has ‘other’ and isempty(ThreatTypes)
| summarize make_set(Url), make_set(SenderFromDomain) by case1, Timestamp_YYYYMMDD, Timestamp_IM, Extracted_IMF_Date, IMF, InternetMessageId, ThreatTypes, Subject,SenderIP=iff(isnotempty(SenderIPv4),SenderIPv4,SenderIPv6), ReportId

Conclusion

Although it may seem that we’ve already covered most detections and day-to-day threat hunting across emails, devices, and users — or that we depend on new attacks to create new detections — spending time on everyday scenarios shows there’s still a lot of untapped value for spotting suspicious patterns and behavior.

The scenarios in this article are just a small slice of the “juice” we can squeeze out of NetworkMessageId. The use of odspnotify value pattern, is extremely useful for companies managing emails that can be exchanged in distinct languages.

Threat Hunting via InternetMessageId (+ KQL Queries) was originally published in Detect FYI on Medium, where people are continuing the conversation by highlighting and responding to this story.

Introduction to Malware Binary Triage (IMBT) Course

Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.

Enroll Now and Save 10%: Coupon Code MWNEWS10

Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.

Article Link: https://detect.fyi/threat-hunting-via-internetmessageid-kql-queries-240d32e6f330?source=rss----d5fd8f494f6a---4