Implement support for replies

This commit is contained in:
2025-01-05 19:55:54 -05:00
parent 834c23735d
commit 1f5889dc39
7 changed files with 64 additions and 9 deletions

View File

@@ -15,8 +15,21 @@ export function buildThreadAddressMatcher(): RegExp {
// In the config, the thread ID is represented with the placeholder: %ID%
const idPrefix = escapeRexString(config.mail.threads.idPrefix)
const idCapture = `(${idPrefix}[^@]+)` // first rule of email: don't be as strict as the RFC
const idCapture = `(${idPrefix}[^@.]+)` // first rule of email: don't be as strict as the RFC (special case: IDs after the prefix cannot contain a period)
const replyPrefix = escapeRexString(config.mail.threads.replyPrefix)
const replyCapture = `(${replyPrefix}[^@.]+)?`
const template = escapeRexString(config.mail.threads.template)
.replace('%ID%', idCapture)
.replace('%REPLY%', replyCapture)
const template = escapeRexString(config.mail.threads.template).replace('%ID%', idCapture)
return new RegExp(`^${template}$`)
}
export function formatThreadAddress(thread: string, replyToHash?: string): string {
let reply = replyToHash ? `${config.mail.threads.replyPrefix}${replyToHash}` : ''
return config.mail.threads.template
.replace('%ID%', thread)
.replace('%REPLY%', reply)
}

View File

@@ -1,11 +1,12 @@
import {getMailboxesToSearch, MailboxIterable} from "../mail/read.ts";
import {withClient} from "../mail/client.ts";
import type {Message, RootData, ThreadData} from "../types.ts";
import type {Message, RootData, ThreadComment, ThreadData} from "../types.ts";
import {AsyncCollection} from "../bones/collection/AsyncCollection.ts";
import {sha256} from "../bones/crypto.ts";
import {config} from "../config.ts";
import { marked } from "marked";
import {sanitizeHtml} from "../mail/sanitize.ts";
import {formatThreadAddress} from "./id.ts";
export async function refreshThreadsEntirely(): Promise<void> {
await withClient(async client => {
@@ -41,6 +42,9 @@ export async function refreshThreadsEntirely(): Promise<void> {
comments: [],
}
// 3 passes required to make this work (works because objects are by-reference)
// Pass 1: create all the ThreadComment instances and make a map by their ID
const commentsByHash: {[hash: string]: ThreadComment} = {}
const messages = messagesByThread[threadId]
for ( const message of messages ) {
if (
@@ -50,7 +54,10 @@ export async function refreshThreadsEntirely(): Promise<void> {
threadData.refresh.markers[message.mailbox] = message.modseq
}
threadData.comments.push({
commentsByHash[message.idHash] = {
idHash: message.idHash,
replyToHash: message.replyToHash,
replyAddress: formatThreadAddress(threadId, message.idHash),
user: {
name: message.from.name || '(anonymous)',
mailId: sha256(message.from.address!.toLowerCase()),
@@ -60,7 +67,30 @@ export async function refreshThreadsEntirely(): Promise<void> {
subject: message.subject,
text: message.content,
rendered: message.html || sanitizeHtml(await marked(message.content)),
})
}
}
// Pass 2: Push replies into the replies array of their immediate parent
for ( const message of messages ) {
if ( !message.replyToHash ) {
continue
}
if ( !commentsByHash[message.replyToHash].replies ) {
commentsByHash[message.replyToHash].replies = []
}
commentsByHash[message.replyToHash].replies!.push(commentsByHash[message.idHash])
}
// Pass 3: push all top-level comments into the ThreadData
for ( const idHash of Object.keys(commentsByHash) ) {
const comment = commentsByHash[idHash]
if ( comment.replyToHash ) {
continue
}
threadData.comments.push(comment)
}
const json = JSON.stringify(