2.8 KiB
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-Toand falls back to the lastReferencesentry. - 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.parseon the emaildatefield (epoch-zero when missing, matching upstream behaviour). - Records
email_processing.thread.grouped_totalwith the produced thread count per call.
Usage: Basic
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
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?)—<id>extraction.isSubjectChange(original, reply)— heuristic.groupThreads(emails)— returns chronologicalEmailThread[].
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.