Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Maintain email conversation context across messages using threading headers. Use when building thread reconstruction, linking replies to conversations, detecting thread hijacking, stripping quoted content, or providing thread context to AI agents.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 316% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 247% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 193% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 129% | 0% |
Keep email conversations connected - link replies to their threads, reconstruct conversation history, and detect when threads go wrong.
inbound-processing - receiving and parsing incoming email (prerequisite for threading)reply-classification - classifying reply intent once you have thread contextemail-security - injection and phishing prevention, including thread hijackingtransactional-email - sending emails that need proper threading headersEmail threading is built on three RFC 5322 headers. Every email client uses some combination of these to decide which messages belong to the same conversation.
Message-ID - a globally unique identifier for each email message. Generated by the sending mail server or client.
Message-ID: <abc123.456@mail.example.com>Format: <local-part@domain>. The local part is typically a UUID, timestamp, or random string. The domain should be the sending server's hostname. Always enclosed in angle brackets.
In-Reply-To - contains the Message-ID of the message being replied to. Only present on replies, not on original messages.
In-Reply-To: <abc123.456@mail.example.com>References - contains the Message-IDs of all ancestors in the thread, oldest first. Grows with each reply.
References: <abc123.456@mail.example.com>
<def789.012@reply.example.com>
<ghi345.678@reply2.example.com>Original message:
Message-ID: <msg-001@sender.com>
Subject: Q3 proposalFirst reply:
Message-ID: <msg-002@recipient.com>
In-Reply-To: <msg-001@sender.com>
References: <msg-001@sender.com>
Subject: Re: Q3 proposalReply to the reply:
Message-ID: <msg-003@sender.com>
In-Reply-To: <msg-002@recipient.com>
References: <msg-001@sender.com> <msg-002@recipient.com>
Subject: Re: Q3 proposalThe References header creates a chain. Any client can reconstruct the full thread tree from it.
A Message-ID must be globally unique. Bad Message-IDs break threading because clients cannot distinguish messages.
Good patterns:
<{uuid}@{your-sending-domain}>
<{timestamp}.{random}@{your-sending-domain}>Bad patterns:
<1@localhost> # Not unique
<msg@example.com> # Reused across messages
<no-angle-brackets> # Missing required angle bracketsUse your actual sending domain as the right-hand side. Some providers generate Message-IDs for you - if you need to reference them later (for threading inbound replies), store the provider-assigned Message-ID at send time.
Gmail, Outlook, and other clients use different algorithms to group messages into conversations. If your emails thread correctly in one client but not another, this section explains why.
Gmail uses a multi-factor algorithm combining headers and subject:
Re:, Fwd:, and similar prefixes. If the subject changes, Gmail breaks the thread.Gmail-specific behaviors:
threadId internally. You can query it via the Gmail API (threads.list, threads.get), but it is user-specific - the same conversation has different thread IDs for different participants.threadId on the message resource AND include correct References and In-Reply-To headers AND keep the subject matching.Outlook's conversation view groups primarily by subject line:
RE:, FW:, AW:, SV:, etc. across languages).Outlook-specific behaviors:
ConversationId in Microsoft Graph API) is assigned based on the subject hash and is shared across participants, unlike Gmail's per-user thread IDs.ConversationIndex is a binary header Outlook uses internally for tree ordering. Do not try to generate this yourself unless you are building Outlook integrations specifically.To thread correctly across both Gmail and Outlook:
In-Reply-To to the parent message's Message-IDReferences to the parent's References + parent's Message-IDRe: only, do not modify the base subject)When you receive inbound email and need to rebuild the conversation, you have two linking strategies.
Match the inbound message's In-Reply-To header against stored outbound Message-IDs.
Inbound arrives with:
In-Reply-To: <send-abc123@mail.yourapp.com>
Look up in your database:
SELECT request_id FROM send_attempts
WHERE provider_message_id = 'send-abc123@mail.yourapp.com'This gives you a direct, high-confidence link (confidence = 1.0) back to the specific outbound message being replied to. From there, you can reconstruct the full thread.
If In-Reply-To does not match, fall back to parsing the References header. It contains all ancestor Message-IDs, so any match connects you to the thread.
When headers do not match (common when emails are forwarded, or the recipient's client strips headers), fall back to heuristics:
Re:, Fwd: prefixes and compare normalized subjectsHeuristic linking should be flagged as lower confidence (e.g., 0.5) so downstream systems can treat it accordingly.
typescript// Example: heuristic fallback linking type LinkResult = { requestId: string; method: 'in_reply_to' | 'references' | 'recipient_recent' | 'subject_match'; confidence: number; // 1.0 for header match, 0.3-0.7 for heuristics };
For reliable thread reconstruction, persist these fields for every inbound and outbound message:
| Field | Why | |-------|-----| | message_id | Your internal ID | | provider_message_id | The Message-ID assigned by the provider/MTA | | in_reply_to | The In-Reply-To header value | | references_header | The full References header (space-separated Message-IDs) | | thread_id | Your internal thread/conversation ID | | from_email | Sender address | | to_email | Recipient address | | subject | For fallback matching | | created_at | For time-window heuristics |
When a reply arrives, the message body contains both the new content and quoted previous messages. Extracting just the new content is essential for AI agents, search indexing, and clean display.
There is no standard for how email clients format quoted replies. Every client does it differently:
| Client | Plain text format | HTML format | |--------|------------------|-------------| | Gmail | > prefix per line | <div class="gmail_quote"> wrapper | | Outlook | > or full block | <div id="appendonsend"> or <!--[if gte mso 9]> | | Apple Mail | > prefix | <blockquote type="cite"> | | Thunderbird | > prefix | <blockquote> with cite attribute |
For plain text emails, detect quoted lines by these patterns:
Lines starting with ">"
Lines starting with "On <date>, <name> wrote:"
Lines starting with "From: " followed by header-like content
Lines matching "-----Original Message-----"
Lines matching "________________________________" (Outlook separator)A basic approach: scan for the first line matching a reply header pattern, then treat everything from that point forward as quoted content.
typescriptconst QUOTE_PATTERNS = [ /^>+ /m, // > prefix /^On .+ wrote:$/m, // Gmail/Apple "On ... wrote:" /^-{2,}\s*Original Message\s*-{2,}/im, // Outlook separator /^_{10,}/m, // Outlook underscore line /^From:\s.+/m, // Forwarded header block /^Sent from my /m, // Mobile signatures (not quotes, but noise) ]; function extractNewContent(bodyText: string): string { let cutPoint = bodyText.length; for (const pattern of QUOTE_PATTERNS) { const match = pattern.exec(bodyText); if (match && match.index < cutPoint) { cutPoint = match.index; } } return bodyText.slice(0, cutPoint).trim(); }
For HTML emails, target the wrapper elements:
typescriptconst HTML_QUOTE_SELECTORS = [ 'div.gmail_quote', // Gmail 'div.yahoo_quoted', // Yahoo 'blockquote[type="cite"]', // Apple Mail 'div#appendonsend', // Outlook web 'div.moz-cite-prefix', // Thunderbird 'div[id^="divRplyFwdMsg"]', // Outlook desktop ];
Remove elements matching these selectors from the parsed DOM. What remains is the new content.
Do not build your own quoted content parser from scratch. The edge cases are numerous and provider-specific. Proven open-source options:
If you are building for production, start with one of these and customize for edge cases you encounter with your specific user base.
When feeding email threads to an AI agent or LLM, the way you structure thread context significantly affects response quality.
Rather than dumping raw email content, build a structured timeline:
typescripttype ThreadEntry = { direction: 'inbound' | 'outbound'; fromEmail: string; timestamp: string; newContent: string; // Quoted content stripped intent?: string; // Classification result if available metadata?: { hasAttachments: boolean; isForwarded: boolean; confidence: number; // Link confidence }; }; type ThreadContext = { threadId: string; subject: string; participants: string[]; timeline: ThreadEntry[]; // Chronological order totalMessages: number; firstMessageAt: string; lastMessageAt: string; };
Long email threads create context window pressure. Strategies for managing this:
Beyond raw message content, enrich the thread context with:
typescripttype EnrichedThreadContext = { timeline: ThreadEntry[]; suppressionStatus: { suppressed: boolean; reasonCode?: string }; activeIncidents: Array<{ id: string; type: string; status: string }>; lastClassification: { intent: string; confidence: number } | null; };
This enriched context lets AI agents make informed decisions - they know not to send a follow-up to a suppressed contact, or to escalate when there are open incidents.
Email threads are a trust signal. When someone replies in an existing thread, users (and AI agents) assume continuity. Attackers exploit this.
Thread hijacking (also called reply chain attacks) is when an attacker inserts themselves into an existing conversation thread. This is more dangerous than regular phishing because:
Attack methods:
In-Reply-To and References headers pointing to a real thread, making their message appear in the conversationexamp1e.com vs example.com) and replies with correct threading headersMonitor thread history for these suspicious patterns:
Forged thread injection - a new sender appears in a thread they have never participated in. Flag any message where the from_email has not been seen in the thread history.
Intent flip from new sender - a new participant arrives with an intent that conflicts with the conversation's established direction. For example, a thread where the contact has been "interested" suddenly gets a reply from a different sender with intent "objection" or "legal".
Rapid intent flip - the same thread switches between conflicting intents (e.g., "interested" to "objection") within a short time window (30 minutes or less). This can indicate a compromised account being used to derail a conversation.
typescripttype ThreadAnomaly = { type: 'forged_thread_injection' | 'intent_flip_different_sender' | 'rapid_intent_flip'; detail: string; }; type ThreadAnomalyResult = { isAnomalous: boolean; anomalies: ThreadAnomaly[]; severity: 'none' | 'warning' | 'critical'; };
Severity escalation:
Not all intent changes are suspicious. "Interested" to "question" is natural. These pairs should be flagged as conflicts:
| Intent A | Intent B | |----------|----------| | interested | objection | | interested | legal | | interested | security | | support | objection |
Sometimes conversations need to be split into separate threads or merged together.
Create a new thread by sending a message with:
Message-IDIn-Reply-To or References headers (or pointing only to the specific message you are branching from)Gmail will break this into a new conversation because the subject changed. Outlook may still group it if the subject is close enough - changing the subject more significantly forces a split in both clients.
You cannot force email clients to merge threads client-side. But in your application:
thread_id to messages from both conversationsX-Thread-ID instead.threadId is per-user. Outlook's ConversationId is subject-based. Neither is stable enough to be your canonical thread identifier. Generate your own internal thread IDs and map provider IDs to them.Other measured skills in the registry, with their headline benchmark lift.