52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
/**
|
|
* RFC 5322 §3.6.4 reply threading.
|
|
*
|
|
* Computes the In-Reply-To and References headers an outgoing reply must
|
|
* carry so MUAs can stitch the conversation back together.
|
|
*
|
|
* In-Reply-To = parent.Message-ID
|
|
* References = parent.References (if any) + parent.Message-ID
|
|
*
|
|
* Bare msg-ids only — angle brackets are stripped because JMAP RFC 8621
|
|
* §4.1.2.3 stores Message-IDs without them.
|
|
*/
|
|
|
|
export interface ParentThreadingInfo {
|
|
// JMAP RFC 8621 §4.1.2.3 specifies messageId as String[]|null, but the
|
|
// codebase has historically typed it as string. Accept either shape.
|
|
messageId?: string | string[];
|
|
references?: string[];
|
|
}
|
|
|
|
export interface ReplyThreadingHeaders {
|
|
inReplyTo: string[];
|
|
references: string[];
|
|
}
|
|
|
|
export function stripMessageIdBrackets(id: string): string {
|
|
return id.trim().replace(/^<+/, '').replace(/>+$/, '').trim();
|
|
}
|
|
|
|
export function computeReplyThreadingHeaders(
|
|
parent: ParentThreadingInfo | undefined,
|
|
): ReplyThreadingHeaders | null {
|
|
const rawId = Array.isArray(parent?.messageId) ? parent.messageId[0] : parent?.messageId;
|
|
const parentId = rawId ? stripMessageIdBrackets(rawId) : '';
|
|
if (!parentId) return null;
|
|
|
|
const ancestors = (parent?.references ?? [])
|
|
.map(stripMessageIdBrackets)
|
|
.filter(Boolean);
|
|
|
|
// De-dupe while preserving order; the parent's id closes the chain.
|
|
const seen = new Set<string>();
|
|
const references: string[] = [];
|
|
for (const id of [...ancestors, parentId]) {
|
|
if (seen.has(id)) continue;
|
|
seen.add(id);
|
|
references.push(id);
|
|
}
|
|
|
|
return { inReplyTo: [parentId], references };
|
|
}
|