first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# thread-grouper
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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?)` — `<id>` 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.