From 1309db5e43873f5b2b8c95c74d383b21d8193db0 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Thu, 7 Aug 2025 23:28:12 +0200 Subject: [PATCH] groupware: add identities of all accounts to the index resource --- pkg/jmap/jmap_api_identity.go | 48 ++++++++++++++++++ pkg/jmap/jmap_model.go | 2 +- pkg/jmap/jmap_tools.go | 45 +++++++++++++++++ .../pkg/groupware/groupware_api_index.go | 49 +++++++++++++------ .../pkg/groupware/groupware_framework.go | 14 ++++++ 5 files changed, 143 insertions(+), 15 deletions(-) diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go index 84c0b342f1..5e6a7b18c5 100644 --- a/pkg/jmap/jmap_api_identity.go +++ b/pkg/jmap/jmap_api_identity.go @@ -2,8 +2,10 @@ package jmap import ( "context" + "strconv" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/rs/zerolog" ) // https://jmap.io/spec-mail.html#identityget @@ -20,3 +22,49 @@ func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Con return response, simpleError(err, JmapErrorInvalidJmapResponsePayload) }) } + +type IdentitiesGetResponse struct { + State string `json:"state"` + Identities map[string][]Identity `json:"identities,omitempty"` + NotFound []string `json:"notFound,omitempty"` +} + +func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, Error) { + uniqueAccountIds := uniq(accountIds) + + logger = j.loggerParams("", "GetIdentities", session, logger, func(l zerolog.Context) zerolog.Context { + return l.Array(logAccountId, logstrarray(uniqueAccountIds)) + }) + + calls := make([]Invocation, len(uniqueAccountIds)) + for i, accountId := range uniqueAccountIds { + calls[i] = invocation(IdentityGet, IdentityGetCommand{AccountId: accountId}, strconv.Itoa(i)) + } + + cmd, err := request(calls...) + if err != nil { + return IdentitiesGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err} + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentitiesGetResponse, Error) { + identities := make(map[string][]Identity, len(uniqueAccountIds)) + lastState := "" + notFound := []string{} + for i, accountId := range uniqueAccountIds { + var response IdentityGetResponse + err = retrieveResponseMatchParameters(body, IdentityGet, strconv.Itoa(i), &response) + if err != nil { + return IdentitiesGetResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload) + } else { + identities[accountId] = response.List + } + lastState = response.State + notFound = append(notFound, response.NotFound...) + } + + return IdentitiesGetResponse{ + Identities: identities, + State: lastState, + NotFound: uniq(notFound), + }, nil + }) +} diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index a59bc8eacf..9cb7c51d41 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -1568,7 +1568,7 @@ type IdentityGetResponse struct { AccountId string `json:"accountId"` State string `json:"state"` List []Identity `json:"list,omitempty"` - NotFound []any `json:"notFound,omitempty"` + NotFound []string `json:"notFound,omitempty"` } type VacationResponseGetCommand struct { diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go index 3402ba6d34..b02f3246b4 100644 --- a/pkg/jmap/jmap_tools.go +++ b/pkg/jmap/jmap_tools.go @@ -10,6 +10,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/rs/zerolog" ) type eventListeners[T any] struct { @@ -222,3 +223,47 @@ func (i *Invocation) UnmarshalJSON(bs []byte) error { i.Parameters = params return nil } + +const logMaxStrLength = 1024 + +// Safely caps a string to a given size to avoid log bombing. +// Use this function to wrap strings that are user input (HTTP headers, path parameters, URI parameters, HTTP body, ...). +func logstr(text string) string { + runes := []rune(text) + + if len(runes) <= logMaxStrLength { + return text + } else { + return string(runes[0:logMaxStrLength-1]) + `\u2026` // hellip + } +} + +type SafeLogStringArrayMarshaller struct { + array []string +} + +func (m SafeLogStringArrayMarshaller) MarshalZerologArray(a *zerolog.Array) { + for _, elem := range m.array { + a.Str(logstr(elem)) + } +} + +var _ zerolog.LogArrayMarshaler = SafeLogStringArrayMarshaller{} + +func logstrarray(array []string) SafeLogStringArrayMarshaller { + return SafeLogStringArrayMarshaller{array: array} +} + +func uniq[T comparable](ary []T) []T { + m := map[T]bool{} + for _, v := range ary { + m[v] = true + } + set := make([]T, len(m)) + i := 0 + for v := range m { + set[i] = v + i++ + } + return set +} diff --git a/services/groupware/pkg/groupware/groupware_api_index.go b/services/groupware/pkg/groupware/groupware_api_index.go index b89c8f1f27..584f8a05a8 100644 --- a/services/groupware/pkg/groupware/groupware_api_index.go +++ b/services/groupware/pkg/groupware/groupware_api_index.go @@ -2,6 +2,8 @@ package groupware import ( "net/http" + + "github.com/opencloud-eu/opencloud/pkg/jmap" ) type IndexLimits struct { @@ -37,6 +39,7 @@ type IndexAccount struct { IsPersonal bool `json:"isPersonal"` IsReadOnly bool `json:"isReadOnly"` Capabilities IndexAccountCapabilities `json:"capabilities"` + Identities []jmap.Identity `json:"identities,omitempty"` } type IndexPrimaryAccounts struct { @@ -69,28 +72,46 @@ type SwaggerIndexResponse struct { // 200: IndexResponse func (g Groupware) Index(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { + + accountIds := make([]string, len(req.session.Accounts)) + i := 0 + for k := range req.session.Accounts { + accountIds[i] = k + i++ + } + accountIds = uniq(accountIds) + + identitiesResponse, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger) + if err != nil { + return req.errorResponseFromJmap(err) + } + accounts := make(map[string]IndexAccount, len(req.session.Accounts)) - for i, a := range req.session.Accounts { - accounts[i] = IndexAccount{ - Name: a.Name, - IsPersonal: a.IsPersonal, - IsReadOnly: a.IsReadOnly, + for accountId, account := range req.session.Accounts { + indexAccount := IndexAccount{ + Name: account.Name, + IsPersonal: account.IsPersonal, + IsReadOnly: account.IsReadOnly, Capabilities: IndexAccountCapabilities{ Mail: IndexAccountMailCapabilities{ - MaxMailboxDepth: a.AccountCapabilities.Mail.MaxMailboxDepth, - MaxSizeMailboxName: a.AccountCapabilities.Mail.MaxSizeMailboxName, - MaxSizeAttachmentsPerEmail: a.AccountCapabilities.Mail.MaxSizeAttachmentsPerEmail, - MayCreateTopLevelMailbox: a.AccountCapabilities.Mail.MayCreateTopLevelMailbox, - MaxDelayedSend: a.AccountCapabilities.Submission.MaxDelayedSend, + MaxMailboxDepth: account.AccountCapabilities.Mail.MaxMailboxDepth, + MaxSizeMailboxName: account.AccountCapabilities.Mail.MaxSizeMailboxName, + MaxSizeAttachmentsPerEmail: account.AccountCapabilities.Mail.MaxSizeAttachmentsPerEmail, + MayCreateTopLevelMailbox: account.AccountCapabilities.Mail.MayCreateTopLevelMailbox, + MaxDelayedSend: account.AccountCapabilities.Submission.MaxDelayedSend, }, Sieve: IndexAccountSieveCapabilities{ - MaxSizeScriptName: a.AccountCapabilities.Sieve.MaxSizeScript, - MaxSizeScript: a.AccountCapabilities.Sieve.MaxSizeScript, - MaxNumberScripts: a.AccountCapabilities.Sieve.MaxNumberScripts, - MaxNumberRedirects: a.AccountCapabilities.Sieve.MaxNumberRedirects, + MaxSizeScriptName: account.AccountCapabilities.Sieve.MaxSizeScript, + MaxSizeScript: account.AccountCapabilities.Sieve.MaxSizeScript, + MaxNumberScripts: account.AccountCapabilities.Sieve.MaxNumberScripts, + MaxNumberRedirects: account.AccountCapabilities.Sieve.MaxNumberRedirects, }, }, } + if identity, ok := identitiesResponse.Identities[accountId]; ok { + indexAccount.Identities = identity + } + accounts[accountId] = indexAccount } return response(IndexResponse{ diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index 9109dc3f94..5570d893ff 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -574,3 +574,17 @@ func (g Groupware) NotFound(w http.ResponseWriter, r *http.Request) { render.Status(r, http.StatusNotFound) w.WriteHeader(http.StatusNotFound) } + +func uniq[T comparable](ary []T) []T { + m := map[T]bool{} + for _, v := range ary { + m[v] = true + } + set := make([]T, len(m)) + i := 0 + for v := range m { + set[i] = v + i++ + } + return set +}