# thread-grouper _(no submodules)_ ## Purpose Groups a flat list of emails into conversation threads using In-Reply-To, References, and subject-line heuristics. Each thread is returned with its full chronological message list plus a parent / child tree rooted at the earliest message, so consumers can render either a flat timeline or a nested conversation view. ## Features - Parent resolution prefers `In-Reply-To` and falls back to the last `References` entry. - Subject-line normalisation strips `Re:`, `Fwd:`, `Aw:`, `Sv:`, `Vs:` prefixes (with optional `[N]` ticket-tracker suffixes). - A reply with a significantly different subject starts a new thread even when it references the original — heuristic catches "hijacked" reply chains. - Stable chronological ordering via `Date.parse` on the email `date` field (epoch-zero when missing, matching upstream behaviour). - Records `email_processing.thread.grouped_total` with the produced thread count per call. ## Usage: Basic ```ts import { groupThreads } from "@suaveplan/email-processing/threading"; const threads = groupThreads( messages.map((m) => ({ messageId: m.messageId ?? "", inReplyTo: m.inReplyTo, references: m.references, subject: m.subject, date: m.date, })) ); for (const t of threads) { console.log(t.threadId, t.subject, t.messageCount); } ``` ## Usage: Advanced ```ts import { groupThreads, normalizeSubject, isSubjectChange, parseReferences, } from "@suaveplan/email-processing/threading"; const threads = groupThreads(messages); function flatten(node) { return [node.message, ...node.children.flatMap(flatten)]; } const summary = threads.map((t) => ({ threadId: t.threadId, subject: normalizeSubject(t.subject ?? ""), flat: flatten(t.root).map((m) => m.messageId), refs: parseReferences(t.messages.at(-1)?.references), })); ``` ## API Reference - `ThreadableEmail` — `{ messageId, inReplyTo?, references?, subject?, date?, from? }`. - `ThreadNode` — `{ message, children }`. - `EmailThread` — `{ threadId, subject?, root, messageCount, messages }`. - `normalizeSubject(subject)`. - `parseReferences(references?)` — `` extraction. - `isSubjectChange(original, reply)` — heuristic. - `groupThreads(emails)` — returns chronological `EmailThread[]`. ## Implementation Notes `buildTree` falls back to a `"messageId: ''"` placeholder root only when the input array is unexpectedly empty — `groupThreads` itself short-circuits empty input, so the fallback exists purely for type safety. The grouper's chronological pass uses the most-recent reference (last entry in `References`) when `In-Reply-To` is absent, matching most MUA conventions. Subject-change detection is case-insensitive and ignores whitespace differences.