Files
skills/docs/communication/emails/email-processing/src/thread-grouper/thread-grouper.md
2026-05-29 08:20:56 +01:00

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-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

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 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.