feat(protocols): conversations - thread_id and in_reply_to written at delivery, list and read APIs
A reply now joins the conversation of the message it answers (Message-ID,
In-Reply-To, References), falling back to the subject for replies with no
headers. /mail/threads lists a folder as conversations and /mail/thread/{id}
returns one. Replying from the webmail flags the answered message.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff89e9dc44
commit
67d0c8aa5a
17 files changed
+1025
-9
No files matched your search
@@ -124,6 +124,11 @@ func persistOne(ctx context.Context, tx pgx.Tx, input port.PersistInboundMessage
|
||||
// id and received_at come back because message_flags is keyed on both:
|
||||
// email_messages is partitioned by received_at, so the flag rows cannot be
|
||||
// written without the same timestamp the row landed on.
|
||||
threadID, err := resolveThread(ctx, tx, input)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var messageID uuid.UUID
|
||||
var receivedAt time.Time
|
||||
err = tx.QueryRow(ctx, `
|
||||
@@ -131,8 +136,8 @@ func persistOne(ctx context.Context, tx pgx.Tx, input port.PersistInboundMessage
|
||||
mailbox_id, folder_id, uid, modseq, blob_id,
|
||||
sender_address, recipient_addresses, size_bytes,
|
||||
subject, snippet, from_display_name, message_id_header, has_attachments,
|
||||
spf_result, dkim_result, dmarc_result
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
spf_result, dkim_result, dmarc_result, in_reply_to, thread_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
RETURNING id, received_at
|
||||
`, input.MailboxID, folderID, uid, modseq, input.Blob.ID,
|
||||
input.SenderAddress, []string{input.RecipientAddress}, input.Blob.SizeBytes,
|
||||
@@ -147,7 +152,8 @@ func persistOne(ctx context.Context, tx pgx.Tx, input port.PersistInboundMessage
|
||||
// Sent copy and drafts have no authentication results by definition -
|
||||
// and it failed the whole insert. Sent stayed empty because that write
|
||||
// is deliberately non-fatal, and saving a draft reported an error.
|
||||
nullIfEmpty(input.SPFResult), nullIfEmpty(input.DKIMResult), nullIfEmpty(input.DMARCResult)).
|
||||
nullIfEmpty(input.SPFResult), nullIfEmpty(input.DKIMResult), nullIfEmpty(input.DMARCResult),
|
||||
nullIfEmpty(input.InReplyTo), threadID).
|
||||
Scan(&messageID, &receivedAt)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert email_messages: %w", err)
|
||||
|
||||
Reference in new issue
Block a user