Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Have you ever tried to decode the Thread-Index header field? #1103

Open
Sicos1977 opened this issue Nov 22, 2024 · 4 comments
Open
Labels
question A question about how to do something

Comments

@Sicos1977
Copy link

Sicos1977 commented Nov 22, 2024

I have a use case where I want to use the Thread-Index header to link messages together (Yes I know there is also the References field but that one is not always filled in my case). I tried decoded this header AQHbJet7Z+efu/5M5UWYnpinBaQePrKfAKzegAAO5bCAAAHygIAAD3LwgAG3uyCAAAECjYAXUgfggASoxyCAAAqegIADX0fwgAFtahCAAAThwIAAAMtwgAAAupCAAAEUEIAAImAggAAHlkCAAC0xcA== with some example code that I found on the internet.

        try
        {
            Raw = threadIndex;

            var bytes = Convert.FromBase64String(threadIndex);

            // Thread index length should be 22 plus extra 5 bytes per reply
            if (bytes.Length < 22 || (bytes.Length - 22) % 5 != 0)
                return;

            Id = new Guid(bytes.Skip(6).Take(16).ToArray());
            var childBlockCount = (bytes.Length - 22) / 5;

            if (childBlockCount == 0)
                Date = DateTime.FromFileTimeUtc(bytes.Skip(1).Take(6).Select(b => (long)b).Aggregate((l1, l2) => (l1 << 8) + l2) << 16).ToLocalTime();
            else
            {
                Date = DateTime.FromFileTimeUtc(bytes.Take(6).Select(b => (long)b).Aggregate((l1, l2) => (l1 << 8) + l2) << 16).ToLocalTime();
                Dates = [Date];

                for (var i = 0; i < childBlockCount; i++)
                {
                    var childTicks = bytes.Skip(22 + i * 5).Take(4).Select(b => (long)b).Aggregate((l1, l2) => (l1 << 8) + l2) << 18;
                    childTicks &= ~((long)1 << 50);
                    Date = Date.AddTicks(childTicks);
                    if (i < childBlockCount - 1) Dates.Add(Date);
                }
            }
        }
        catch 
        {
            // Ignore
        }

With some headers this works but most of the times I just get dated in the year 1830 or in 1601 so something is different.

I also tried following this documentation --> https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/tracking-conversations?redirectedfrom=MSDN ... but still no luck.

internal ThreadIndex(string threadIndex)
{
    try
    {
        Raw = threadIndex;

        // Decode base64 string to bytes
        var bytes = Convert.FromBase64String(threadIndex);

        // Validate minimum length (22 bytes for the header block)
        if (bytes.Length < 22 || (bytes.Length - 22) % 5 != 0)
            return;

        // Parse the header block
        if (bytes[0] != 1) // Reserved byte must be 1
            return;

        // Extract and compute the date from the FILETIME format in the header
        var headerTicks = bytes.Skip(1).Take(5).Select(b => (long)b).Aggregate((l1, l2) => (l1 << 8) + l2) << 16;
        Date = DateTime.FromFileTimeUtc(headerTicks).ToLocalTime();

        // Extract GUID from the header block
        Id = new Guid(bytes.Skip(6).Take(16).ToArray());

        // Prepare the list of dates
        Dates = new List<DateTime> { Date };

        // Parse child blocks (if present)
        var childBlockCount = (bytes.Length - 22) / 5;
        for (var i = 0; i < childBlockCount; i++)
        {
            // Extract each child block (5 bytes)
            var childBytes = bytes.Skip(22 + i * 5).Take(5).ToArray();

            // Extract the first bit (time encoding strategy)
            var strategyBit = (childBytes[0] & 0b10000000) != 0;

            // Extract the 31 bits for time difference
            var timeDiff = ((childBytes[0] & 0b01111111) << 24)
                         | (childBytes[1] << 16)
                         | (childBytes[2] << 8)
                         | childBytes[3];

            if (strategyBit)
                timeDiff <<= 23; // High-precision, shift low 23 bits
            else
                timeDiff <<= 18; // Low-precision, shift low 18 bits

            // Mask out the 50th bit to handle the encoding properly
            timeDiff &= ~((long)1 << 50);

            // Compute the child date using the time difference
            var childDate = Date.AddTicks(timeDiff);
            Dates.Add(childDate);
        }
    }
    catch
    {
        // Ignore exceptions for invalid input
    }
}

So I was wondering if you ever tried to do something like this with MimeKit?

@Sicos1977
Copy link
Author

Note: I found your ThreadIndex implementation in mimekit but that also gives random junk dates when trying to decode the threadindex string in the post above.

@Sicos1977
Copy link
Author

For the moment I just gave up, I think the documentation on the Microsoft site is missing something and without knowing what it seems impossible to crack this egg.

@jstedfast
Copy link
Owner

I don't think I've ever tried to decode it, but now you have me interested haha.

@Sicos1977
Copy link
Author

Sicos1977 commented Nov 23, 2024

Well give it a try, it drove me nuts this week because every time I thought I had everything working another thread-index failed with weird dates. For some reason I sometimes have to get the first 6 bytes from the header for a correct date time even when the specifications say that the first byte is reserved and always 1. In another situation I had to skip the first byte (specification) and then take the next 6 instead of 5....

I also had problems with the child blocks. When the header date was correct and there are for example 16 child block most of the blocks give correct dates but than a few of them give a date in the future ... like 2025 instead of 2024 ... so weird.

So that is why I think something is missing in the documentation on the Microsoft site. I also searched Google and asked ChatGPT but even that came up with not always working code. Al so other people seem to have the same problems that I ran into.

Also tried to find some C++ of working code in another language (so that I could port it) .... but al so no luck

@jstedfast jstedfast added the question A question about how to do something label Nov 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A question about how to do something
Projects
None yet
Development

No branches or pull requests

2 participants