skills/inbound/thread-management/SKILL.md
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.
npx skillsauth add chunkydotdev/email-skills thread-managementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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: <[email protected]>
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: <[email protected]>
References - contains the Message-IDs of all ancestors in the thread, oldest first. Grows with each reply.
References: <[email protected]>
<[email protected]>
<[email protected]>
Original message:
Message-ID: <[email protected]>
Subject: Q3 proposal
First reply:
Message-ID: <[email protected]>
In-Reply-To: <[email protected]>
References: <[email protected]>
Subject: Re: Q3 proposal
Reply to the reply:
Message-ID: <[email protected]>
In-Reply-To: <[email protected]>
References: <[email protected]> <[email protected]>
Subject: Re: Q3 proposal
The 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
<[email protected]> # Reused across messages
<no-angle-brackets> # Missing required angle brackets
Use 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: <[email protected]>
Look up in your database:
SELECT request_id FROM send_attempts
WHERE provider_message_id = '[email protected]'
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.
// 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.
const 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:
const 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:
type 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:
type 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.
type 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 conversationsNot storing outbound Message-IDs. If you do not persist the Message-ID from your sends, you cannot link inbound replies back to their thread. Store it at send time, not later.
Setting In-Reply-To but not References. Some clients (especially Outlook) rely on References for thread ordering. Always set both headers on replies.
Changing the subject line on replies. Adding "[TICKET-123]" or "[ACTION REQUIRED]" to the subject breaks threading in Gmail. Append tracking tokens to the body or use custom headers like X-Thread-ID instead.
Treating all threads as linear. Email threads are trees, not lists. A single message can have multiple replies, creating branches. Your data model should support parent-child relationships, not just sequential ordering.
Trusting thread membership implicitly. A message appearing in a thread does not mean it is from a trusted sender. Validate the sender against the thread's participant history, especially before AI agents act on the content.
Building your own quoted reply parser. The edge cases across email clients, languages, and forwarding patterns are enormous. Use an established library (email_reply_parser, planer, talon) and customize from there.
Feeding full thread content to LLMs. Quoted replies duplicate content across messages. Strip quotes before building thread context, or you waste tokens on redundant content and risk confusing the model with conflicting versions.
Ignoring thread length limits. Gmail caps at 100 messages per thread. Your internal thread model should handle this gracefully - do not assume a thread ID maps to one conversation forever.
Using provider-specific thread IDs as your primary key. Gmail's 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.
Not checking authentication on anomalous thread messages. A new sender in a thread who fails SPF/DKIM/DMARC is far more suspicious than one who passes. Always cross-reference thread anomalies with authentication results.
data-ai
Choose and configure an email service provider. Use when setting up email for a new project, comparing providers, migrating between providers, or adding failover.
development
Set up SPF, DKIM, and DMARC email authentication. Use when configuring a new sending domain, debugging spam/rejection issues, adding email providers, or preparing for Google/Yahoo/Microsoft bulk sender requirements.
development
Design and send transactional emails. Use when building password resets, receipts, shipping notifications, account alerts, or separating transactional from marketing streams.
development
Build welcome and activation email sequences. Use when designing signup flows, driving users to key actions, converting trials to paid, or reducing early churn.