The Conversation Index MAPI property piqued my interest long ago as a potentially valuable digital forensics artifact. I wrote a blog about it in 2013, and have been happy to hear that the artifact has been instrumental in solving numerous cases.
What we know about Conversation Index has changed since then. This article is intended as a follow-up to my original blog to discuss how newer Conversation Indices (I will call these “modern”) differ from the original, “classic” Conversation Indices. If you have not read the original blog, I recommend doing so before you proceed.
Thread Index vs. Conversation Index
Contents of the Conversation Index MAPI property are often Base64 encoded and transmitted in a MIME header named “Thread-Index”. Here is what a Thread-Index header may look like:
Thread-Index: Ac3pCr/g148OQoCCQSCy8dDjwH7QBwAAzLowAAARRGA=
When encoded in Base64, the “classic” Thread Index values typically start with “Ab”, “Ac”, “Ad”, or “Ae” while the “modern” ones typically start with “AQH”.
Layout
Let’s take a look at the following Conversation Index:
0101DCCE985315F83A00F6984B6C8FF8236D5A0B3BCDB5E39A588080000048808000020F2F8004467680800022E6808000074A80
As in the classic version, the Conversation Index comprises a 22-byte header block and 0 or more 5-byte child blocks. The last 16 bytes of the header block is a GUID. So, we can organize the Conversation Index as follows:
| Timestamp | GUID | Child Block 1 | Child Block 2 | Child Block 3 | Child Block 4 | Child Block 5 | Child Block 6 |
| 0101DCCE9853 | 15F83A00-F698-4B6C-8FF8-236D5A0B3BCD | B5E39A5880 | 8000004880 | 8000020F2F | 8004467680 | 800022E680 | 8000074A80 |
Header Timestamp
In classic Conversation Indices, there is no wasted byte in the header timestamp. The initial 01 byte and the following 5 bytes form the first 6 bytes of a FILETIME value, which we pad with two bytes of zeros. In the modern version, there is an extra 01 byte prefix which is dropped, and the remainder forms the first 5 bytes of a FILETIME value, to be padded with three bytes of zeros. The side effect of this change is precision loss. In the classic variant, we were missing the precision from the dropped 2 bytes, which left us with ~0.0066-second precision. In the modern version, we are missing the last three bytes, which gives us ~1.68-second precision (2^24 100-nanosecond units = 1,677,721.6 microseconds = ~1.677 seconds).
In any case, the header timestamp calculation is straightforward. We drop the first 01 byte, take the 01DCCE9853 part, pad it to 01DCCE9853000000, and treat that as a hexadecimal FILETIME value in big-endian. In human readable form, this decodes to 2026-04-17 18:30:53.7132032 UTC.
Counterintuitively, we must also calculate the header timestamp the “wrong” way, by treating it as if this were a classic Conversation Index. In that scenario, we start with 0101DCCE9853, pad it with two bytes of zeros to 0101DCCE98530000, and calculate the timestamp as 1831-01-02 18:28:09.1588608. This corresponds to 72581848891588608 100-nanosecond units since the start of January 1, 1601. Let’s call this the “base timestamp”.
Child Block 1
Here is where things start to get interesting. In a modern Conversation Index, the first child block’s time difference is not calculated from the header timestamp, but from the base timestamp.
Our child block looks as follows: B5E39A58 80
The first four bytes contain the 1-bit Delta Code (DC) and 31 bits of Time Difference.
The last byte (0x80) represents the Random Number and Sequence Count. Notably, MS-OXOMSG 2.2.1.3 identifies this byte as “Random (8 bits): Random value generated by using an implementation-specific algorithm.”
In this case, the DC is 1. Which means, per Microsoft’s spec, the high 10 bits and the low 23 bits were discarded. Because the high 10 bits are discarded, the time difference value has a maximum capacity of 54 bits (64 – 10), which allows it to at most represent 2^54 100-nanosecond units. This is 18014398509481984 100-nanosecond units in decimal, and corresponds to approximately 57 years. To refresh our memory, MS-OXOMSG 2.2.1.3 states the following:
-
DC (Delta code) (1 bit) and Time Delta (31 bits): Calculated based on TimeDiff, a 64-bit value representing the difference between the current time and the time stored in the conversation index header:
-
If the difference is less than 1.7 years (high order part of the delta file time bitwise AND with 0x00FE0000 resulting in “0”), the Delta Code field is 0 and the Time Delta field is the least significant 31 bits of the TimeDiff value remaining after the 18 least significant bits are excluded.
-
If the difference is greater than or equal to 1.7 years (high order part of the delta file time bitwise AND with 0x00FE0000 resulting in nonzero), the Delta Code field is 1 and the Time Delta field is the least significant 31 bits of the TimeDiff value remaining after the 23 least significant bits are excluded.
Note that, since the time difference in modern Conversation Indices is calculated from the base timestamp, the time difference is expected to be greater than 1.7 years and DC is expected to be always 1.
One might think that 57 years is eternity in the context of email communications, and that this would have no practical effect. But, because the child block time difference is calculated from the base timestamp, not the header timestamp, the time difference exceeds 57 years–multiple times. When it does, the time difference value silently wraps (i.e., overflows). To account for this, we add the dropped ~57 year time periods until we pass the header timestamp.
Let’s do the math:
- First, let’s calculate the time difference for this child block. We remove the first bit (1), which takes us from 0xB5E39A58 to 0x35E39A58 (904108632 in decimal). Considering the low 23 bits were discarded, we need to multiply this with 2^23, and find 7584212903264256 100-nanosecond intervals (I’ll call these “units” for brevity).
- Our base timestamp was 72581848891588608 units.
- Let’s add 7584212903264256 units to 72581848891588608 units. This gives us 80166061794852864 units and lands us on Jan 14, 1855.
- If we add 18014398509481984 units to make up for 1 overflow, we get to February 15, 1912.
- One more addition of 18014398509481984 units gets us to March 17, 1969.
- Finally, another addition of 18014398509481984 units undoes the overflow and we arrive at 134209257323298816 units. This decodes to 2026-04-17 18:55:32.3298816 UTC. Approximately 24.6 minutes after the header date.
Child Block 2
The calculation for the second child block is simpler, as the time difference is from the last child block.
8000004880 splits as 80000048 80. After we remove the DC bit, the value becomes 0x00000048 (72 in decimal). We multiply by 2^23 to account for the 23 discarded bits, and get to 603979776 units.
We add 603979776 units to 134209257323298816 units and arrive at 134209257927278592 units. This decodes to 2026-04-17 18:56:32.7278592. Approximately 1 minute after Child Block 1.
Remaining Child Blocks
The remaining child blocks are calculated similarly, arriving at the following picture. The output is taken from Forensic Email Intelligence, which supports decoding and deriving Insights from both classic and modern Conversation Indices.
Thread Index: 0101DCCE985315F83A00F6984B6C8FF8236D5A0B3BCDB5E39A588080000048808000020F2F8004467680800022E6808000074A80 GUID: 15f83a00-f698-4b6c-8ff8-236d5a0b3bcd Size: 52 Bytes Header Timestamp: 2026-04-17 18:30:53.7132032 Z Child Count: 6 Child No: 1 Calculated Timestamp: 2026-04-17 18:55:32.3298816 Z Time Delta: 0:00:24:38.6166784 Random No: 8 Sequence Count: 0 Child No: 2 Calculated Timestamp: 2026-04-17 18:56:32.7278592 Z Time Delta: 0:00:01:00.3979776 Random No: 8 Sequence Count: 0 Child No: 3 Calculated Timestamp: 2026-04-17 19:03:54.8075008 Z Time Delta: 0:00:07:22.0796416 Random No: 2 Sequence Count: 15 Child No: 4 Calculated Timestamp: 2026-04-20 12:21:08.5041664 Z Time Delta: 2:17:17:13.6966656 Random No: 8 Sequence Count: 0 Child No: 5 Calculated Timestamp: 2026-04-20 14:26:02.8865536 Z Time Delta: 0:02:04:54.3823872 Random No: 8 Sequence Count: 0 Child No: 6 Calculated Timestamp: 2026-04-20 14:52:08.2008064 Z Time Delta: 0:00:26:05.3142528 Random No: 8 Sequence Count: 0
Caveats and Thoughts
Timestamp Accuracy
I touched on loss of accuracy earlier when discussing the header timestamp differences between classic and modern Conversation Indices. Let’s expand on this a bit:
When we look at the header and child block timestamps, we see them at a 100-nanosecond resolution as the values are transmitted in FILETIME structures. However, the actual resolution is not that high because of the unused bits. For example, in the child block timestamps above, the time difference is stored in the child block as 31 usable bits (compared to a full FILETIME timestamp using 64 bits). If the child block used its 31 bits at full 100-nanosecond resolution, it could only represent a time difference of about 214.7 seconds (2^31 x 100ns). This would cause the value to overflow / wrap (as discussed above) every ~3.6 minutes.
Instead, the algorithm slides the 31-bit window upward to give up resolution while picking up range (this is how we get to the ~57 years discussed above). But, because the low 23 bits are dropped, the smallest change the time difference value can express is 2^23 units, which is about 0.84 seconds (2^23 x 100ns). So, although the output of our calculation yielded a child block timestamp of 2026-04-17 18:55:32.3298816 Z, we cannot assume we have 100-nanosecond precision here.
I will provide a crude analogy to clarify the concept: Imagine we have a thermometer whose scale starts at 32.751 F. But, its resolution is 10 F. So, it shows 32.751 F until the temperature reaches 42.751 F, at which point the display jumps from 32.751 F to 42.751 F. At first glance, the thermometer may appear very precise, but that is not the case. The three decimals (.751) are not a product of precision but reflect the base temperature value, similar to our base timestamp.
Last Byte of Child Blocks
The two Microsoft sources define the last byte of a child block differently. The earlier source states that it is “Four bits containing a random number generated by calling the Win32 function GetTickCount” + “Four bits containing a sequence count that is taken from part of the random number.” while the latter source states “Random (8 bits): Random value generated by using an implementation-specific algorithm“. The example above yielded the same final byte for all child blocks but one. However, other modern Conversation Indices I reviewed contained child blocks with non-similar final bytes.
Sources
Unfortunately, authoritative documentation on how Conversation Indices are calculated is very thin. Especially in the context of modern Conversation Indices with overflowing child block time differences, the findings I present here are largely based on observations and testing rather than official documentation.
A Third Variant
A third type of Thread Index, starting with the 0102 byte pattern, also exists. In Base64 form, these Thread Indices typically start with the “AQI”, “AQJ”, “AQK” or “AQL” pattern. Whether this variant may contain timing information and how it is generated requires further research.
References:
E-mail Conversation Index Analysis for Computer Forensics — https://www.meridiandiscovery.com/how-to/e-mail-conversation-index-metadata-computer-forensics/
Tracking Conversations — https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/tracking-conversations
PidTagConversationIndex Property — https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxomsg/9e994fbb-b839-495f-84e3-2c8c02c7dd9b
Arman Gungor is a certified computer forensic examiner (CCE) and software developer. He has been appointed by courts as a neutral computer forensics expert as well as a neutral eDiscovery consultant. Arman is passionate about doing digital forensics research, developing new investigative techniques, and creating software to support them.