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)
}