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