groupware: add threadSize property in the email summary endpoint

This commit is contained in:
Pascal Bleser
2025-10-21 15:27:56 +02:00
parent 5a3b66c312
commit c61bded13f
3 changed files with 102 additions and 3 deletions

View File

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

View File

@@ -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

View File

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