From c61bded13f7c82b0881046b0dded85eb385aa438 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 21 Oct 2025 15:27:56 +0200 Subject: [PATCH] groupware: add threadSize property in the email summary endpoint --- pkg/jmap/jmap_api_email.go | 89 +++++++++++++++++++ pkg/jmap/jmap_model.go | 5 ++ .../pkg/groupware/groupware_api_emails.go | 11 ++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index 3824fe5bb9..5bdc032cf6 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -902,6 +902,16 @@ type EmailsSummary struct { State State `json:"state"` } +type EmailWithThread struct { + Email + ThreadSize int `json:"threadSize,omitzero"` +} + +type EmailsWithThreadSummary struct { + Emails []EmailWithThread `json:"emails"` + State State `json:"state"` +} + func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint) (map[string]EmailsSummary, SessionState, Language, Error) { logger = j.logger("QueryEmailSummaries", session, logger) @@ -947,3 +957,82 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx return resp, nil }) } + +func (j *Client) QueryEmailSummariesWithThreadCount(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint) (map[string]EmailsWithThreadSummary, SessionState, Language, Error) { + logger = j.logger("QueryEmailSummariesWithThreadCount", session, logger) + + uniqueAccountIds := structs.Uniq(accountIds) + + invocations := make([]Invocation, len(uniqueAccountIds)*3) + for i, accountId := range uniqueAccountIds { + invocations[i*3+0] = invocation(CommandEmailQuery, EmailQueryCommand{ + AccountId: accountId, + Filter: filter, + Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, + Limit: limit, + //CalculateTotal: false, + }, mcid(accountId, "0")) + invocations[i*3+1] = invocation(CommandEmailGet, EmailGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandEmailQuery, + Path: "/ids/*", + ResultOf: mcid(accountId, "0"), + }, + Properties: []string{"id", "threadId", "mailboxIds", "keywords", "size", "receivedAt", "sender", "from", "to", "cc", "bcc", "subject", "sentAt", "hasAttachment", "attachments", "preview"}, + }, mcid(accountId, "1")) + invocations[i*3+2] = invocation(CommandThreadGet, ThreadGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandEmailGet, + Path: "/list/*/threadId", + ResultOf: mcid(accountId, "1"), + }, + }, mcid(accountId, "2")) + } + cmd, err := j.request(session, logger, invocations...) + if err != nil { + return nil, "", "", err + } + + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailsWithThreadSummary, Error) { + resp := map[string]EmailsWithThreadSummary{} + for _, accountId := range uniqueAccountIds { + var response EmailGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &response) + if err != nil { + return nil, err + } + + var thread ThreadGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, mcid(accountId, "2"), &thread) + if err != nil { + return nil, err + } + + threadSizeById := make(map[string]int, len(thread.List)) + for _, thread := range thread.List { + threadSizeById[thread.Id] = len(thread.EmailIds) + } + + if len(response.NotFound) > 0 { + // TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get? + } + + list := make([]EmailWithThread, len(response.List)) + for i, email := range response.List { + ts, ok := threadSizeById[email.ThreadId] + if !ok { + ts = 1 + } + list[i] = EmailWithThread{ + Email: email, + ThreadSize: ts, + } + } + + resp[accountId] = EmailsWithThreadSummary{Emails: list, State: response.State} + } + return resp, nil + }) +} diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index bb81e3c57b..d66f89f4ca 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -3013,6 +3013,11 @@ type ThreadGetCommand struct { Ids []string `json:"ids,omitempty"` } +type ThreadGetRefCommand struct { + AccountId string `json:"accountId"` + IdsRef *ResultReference `json:"#ids,omitempty"` +} + type ThreadGetResponse struct { AccountId string State State diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go index 879d1901e8..5ba0829a21 100644 --- a/services/groupware/pkg/groupware/groupware_api_emails.go +++ b/services/groupware/pkg/groupware/groupware_api_emails.go @@ -1463,6 +1463,9 @@ type EmailSummary struct { // example: $threadId ThreadId string `json:"threadId,omitempty"` + // The number of emails in the thread, including this one. + ThreadSize int `json:"threadSize,omitzero"` + // The set of Mailbox ids this Email belongs to. // // An Email in the mail store MUST belong to one or more Mailboxes at all times (until it is destroyed). @@ -1614,11 +1617,12 @@ type EmailSummary struct { Preview string `json:"preview,omitempty"` } -func summarizeEmail(accountId string, email jmap.Email) EmailSummary { +func summarizeEmail(accountId string, email jmap.EmailWithThread) EmailSummary { return EmailSummary{ AccountId: accountId, Id: email.Id, ThreadId: email.ThreadId, + ThreadSize: email.ThreadSize, MailboxIds: email.MailboxIds, Keywords: email.Keywords, Size: email.Size, @@ -1638,7 +1642,7 @@ func summarizeEmail(accountId string, email jmap.Email) EmailSummary { type emailWithAccountId struct { accountId string - email jmap.Email + email jmap.EmailWithThread } // When the request succeeds. @@ -1731,7 +1735,8 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, logger := log.From(l) - emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit) + // emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit) + emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummariesWithThreadCount(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit) if jerr != nil { return req.errorResponseFromJmap(jerr) }