-- Conversations. -- -- email_messages.thread_id has existed since the first migration and nothing -- filled it, and in_reply_to was only ever copied, never written. A reply and -- the message it answered were therefore unrelated rows, which is why the -- webmail could neither show a conversation nor say whether it had been -- answered. -- -- The subject key lives in the database so delivery, the backfill below and -- anything later share one definition of "the same subject": the prefixes mail -- clients add in several languages are not part of it. CREATE FUNCTION lm_subject_key(subject TEXT) RETURNS TEXT AS $$ SELECT btrim(regexp_replace( lower(coalesce(subject, '')), '^(\s*(re|fw|fwd|enc|res|rv|aw|wg|tr|antw)(\[\d+\])?\s*:)+', '')) $$ LANGUAGE SQL IMMUTABLE; CREATE FUNCTION lm_has_reply_prefix(subject TEXT) RETURNS BOOLEAN AS $$ SELECT lower(coalesce(subject, '')) ~ '^\s*(re|fw|fwd|enc|res|rv|aw|wg|tr|antw)(\[\d+\])?\s*:' $$ LANGUAGE SQL IMMUTABLE; -- Mail that arrived before replies carried their headers cannot be linked by -- Message-ID, so it is grouped by subject, and only where a reply is actually -- in the mailbox: two unrelated messages that merely share a subject stay apart. -- Everything else keeps thread_id NULL, which the webmail reads as "a -- conversation of its own" (COALESCE(thread_id, id)). WITH groups AS ( SELECT DISTINCT mailbox_id, lm_subject_key(subject) AS k FROM email_messages WHERE lm_has_reply_prefix(subject) AND lm_subject_key(subject) <> '' ) UPDATE email_messages m SET thread_id = md5(m.mailbox_id::text || '|' || g.k)::uuid FROM groups g WHERE m.mailbox_id = g.mailbox_id AND lm_subject_key(m.subject) = g.k AND m.thread_id IS NULL; -- The listing groups by this and filters by mailbox and folder. CREATE INDEX idx_msg_mailbox_thread ON email_messages (mailbox_id, (COALESCE(thread_id, id))) WHERE expunged_at IS NULL;