diff --git a/internal/api/handler/unibox.go b/internal/api/handler/unibox.go index 62e5880e..a6b5c234 100644 --- a/internal/api/handler/unibox.go +++ b/internal/api/handler/unibox.go @@ -75,6 +75,12 @@ func (h *Handler) GetUniboxIncoming(c *gin.Context) { params.Sender = &from } + // Address filter: conversations with this person in either direction + // (they sent it or we sent it to them). Powers the compose history panel. + if address := c.Query("address"); address != "" { + params.Address = &address + } + // Parse subject filter if subject := c.Query("subject"); subject != "" { params.Subject = &subject diff --git a/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.down.sql b/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.down.sql new file mode 100644 index 00000000..b1006997 --- /dev/null +++ b/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS idx_unibox_emails_to; diff --git a/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.up.sql b/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.up.sql new file mode 100644 index 00000000..e031dc72 --- /dev/null +++ b/internal/infrastructure/db/migrations/000071_unibox_to_addr_index.up.sql @@ -0,0 +1,4 @@ +-- Compose needs "conversations with this address" lookups that match either +-- side of the exchange. from_addr already has a GIN index; mirror it on +-- to_addr so the address filter and mailbox-affinity scoring stay indexed. +CREATE INDEX IF NOT EXISTS idx_unibox_emails_to ON public.unibox_emails USING gin (to_addr); diff --git a/internal/models/unibox.go b/internal/models/unibox.go index 8a9217b1..f8ce4069 100644 --- a/internal/models/unibox.go +++ b/internal/models/unibox.go @@ -156,7 +156,11 @@ type MailSearchResult struct { } type MailSearchParams struct { - Sender *string + Sender *string + // Address matches either side of the exchange (from_addr OR to_addr), + // giving "every conversation with this person" regardless of direction. + // Substring match on the raw header entries, like Sender. + Address *string Unseen *bool Subject *string Since *time.Time diff --git a/internal/repository/pg_unibox.go b/internal/repository/pg_unibox.go index e7b72a57..efe8480b 100644 --- a/internal/repository/pg_unibox.go +++ b/internal/repository/pg_unibox.go @@ -415,6 +415,17 @@ func (r *uniboxRepository) Search(ctx context.Context, orgID, userID uuid.UUID, argPos++ } + if params.Address != nil && *params.Address != "" { + // Either direction: the contact as sender OR as recipient, so the + // result is the full back-and-forth with that address. + inner += fmt.Sprintf(` AND EXISTS ( + SELECT 1 FROM unnest(ue.from_addr || ue.to_addr) AS f(addr) + WHERE f.addr ILIKE '%%' || $%d || '%%' + )`, argPos) + args = append(args, *params.Address) + argPos++ + } + if len(params.EmailAccountIDs) > 0 { inner += fmt.Sprintf(` AND ue.email_id = ANY($%d)`, argPos) args = append(args, params.EmailAccountIDs)