diff --git a/internal/mail/imap.go b/internal/mail/imap.go index 3536af5..db2acca 100644 --- a/internal/mail/imap.go +++ b/internal/mail/imap.go @@ -283,6 +283,42 @@ func (w *IMAPWatcher) fetchUnseen(c *imapclient.Client) error { return nil } +// resolveThreadID determines the thread_id for a message. +// It tries In-Reply-To first, then falls back to References (last-to-first). +// Returns the message's own ID if no parent is found. +func resolveThreadID(messageID, inReplyTo, references string, lookup func(string) (*database.Task, error)) string { + if inReplyTo != "" { + if parent, err := lookup(inReplyTo); err == nil && parent != nil { + return parent.ThreadID + } + } + + for _, refID := range parseReferences(references) { + if parent, err := lookup(refID); err == nil && parent != nil { + return parent.ThreadID + } + } + + return messageID +} + +// parseReferences splits a References header value into individual message IDs, +// returned in reverse order (most recent first). +func parseReferences(header string) []string { + if header == "" { + return nil + } + raw := strings.Fields(header) + for i := range raw { + raw[i] = strings.TrimSpace(raw[i]) + } + // Reverse: last (most recent) first. + for i, j := 0, len(raw)-1; i < j; i, j = i+1, j-1 { + raw[i], raw[j] = raw[j], raw[i] + } + return raw +} + // processMessage handles a single fetched email message. func (w *IMAPWatcher) processMessage(c *imapclient.Client, buf *imapclient.FetchMessageBuffer, bodySection *imap.FetchItemBodySection) { env := buf.Envelope