From c336b4ced7647ba696d4e76923f2a2d992b3c99a Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Mon, 8 Sep 2025 12:00:36 +0200
Subject: [PATCH] feat(groupware): add fetching all mailboxes for all accounts
* add URL to retrieve all the mailboxes for all the accounts of a user,
as a first use-case for an all-accounts operation, as
/accounts/all/mailboxes
* add URL to retrieve mailbox changes for all the mailboxes of all the
accounts of a user, as a first use-case for an all-accounts
operation, as /accounts/all/mailboxes/changes
* change the defaultAccountId from '*' to '_', as '*' rather indicates
"all" than "default", and we might want to use that for "all
accounts" operations in the future
* refactor(groupware): remove the accountId parameter from the logger()
function, as it is not used anyways, but also confusing for
operations that support multiple account ids
---
pkg/jmap/jmap_api_email.go | 14 +-
pkg/jmap/jmap_api_identity.go | 6 +-
pkg/jmap/jmap_api_mailbox.go | 219 ++++++++++++++----
pkg/jmap/jmap_api_vacation.go | 4 +-
pkg/jmap/jmap_client.go | 6 +-
pkg/jmap/jmap_integration_test.go | 11 +-
pkg/jmap/jmap_test.go | 5 +-
.../pkg/groupware/groupware_api_mailbox.go | 143 +++++++++++-
.../pkg/groupware/groupware_request.go | 20 ++
.../pkg/groupware/groupware_route.go | 20 +-
10 files changed, 375 insertions(+), 73 deletions(-)
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index 2a536aee8..02a799b5e 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -32,7 +32,7 @@ type Emails struct {
// Retrieve specific Emails by their id.
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
- logger = j.logger(accountId, "GetEmails", session, logger)
+ logger = j.logger("GetEmails", session, logger)
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
if maxBodyValueBytes > 0 {
@@ -57,7 +57,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
- logger = j.loggerParams(accountId, "GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Uint(logOffset, offset).Uint(logLimit, limit)
})
@@ -119,7 +119,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c
// Get all the Emails that have been created, updated or deleted since a given state.
func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) {
- logger = j.loggerParams(accountId, "GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
@@ -199,7 +199,7 @@ type EmailSnippetQueryResult struct {
}
func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint) (EmailSnippetQueryResult, SessionState, Error) {
- logger = j.loggerParams(accountId, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Uint(logLimit, limit).Uint(logOffset, offset)
})
@@ -272,7 +272,7 @@ type EmailQueryResult struct {
}
func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryResult, SessionState, Error) {
- logger = j.loggerParams(accountId, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
})
@@ -349,7 +349,7 @@ type EmailQueryWithSnippetsResult struct {
}
func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryWithSnippetsResult, SessionState, Error) {
- logger = j.loggerParams(accountId, "QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
})
@@ -769,7 +769,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
}
func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, Error) {
- logger = j.loggerParams(accountId, "EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
})
diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go
index 768f96c9a..3b3c5357d 100644
--- a/pkg/jmap/jmap_api_identity.go
+++ b/pkg/jmap/jmap_api_identity.go
@@ -15,7 +15,7 @@ type Identities struct {
// https://jmap.io/spec-mail.html#identityget
func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (Identities, SessionState, Error) {
- logger = j.logger(accountId, "GetIdentity", session, logger)
+ logger = j.logger("GetIdentity", session, logger)
cmd, err := request(invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, "0"))
if err != nil {
logger.Error().Err(err)
@@ -44,7 +44,7 @@ type IdentitiesGetResponse struct {
func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, SessionState, Error) {
uniqueAccountIds := structs.Uniq(accountIds)
- logger = j.logger("", "GetIdentities", session, logger)
+ logger = j.logger("GetIdentities", session, logger)
calls := make([]Invocation, len(uniqueAccountIds))
for i, accountId := range uniqueAccountIds {
@@ -91,7 +91,7 @@ type IdentitiesAndMailboxesGetResponse struct {
func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesAndMailboxesGetResponse, SessionState, Error) {
uniqueAccountIds := structs.Uniq(accountIds)
- logger = j.logger("", "GetIdentitiesAndMailboxes", session, logger)
+ logger = j.logger("GetIdentitiesAndMailboxes", session, logger)
calls := make([]Invocation, len(uniqueAccountIds)+1)
calls[0] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: mailboxAccountId}, "0")
diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go
index 111482974..30b1fec17 100644
--- a/pkg/jmap/jmap_api_mailbox.go
+++ b/pkg/jmap/jmap_api_mailbox.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
@@ -14,27 +15,42 @@ type MailboxesResponse struct {
}
// https://jmap.io/spec-mail.html#mailboxget
-func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, SessionState, Error) {
- logger = j.logger(accountId, "GetMailbox", session, logger)
- cmd, err := request(invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, "0"))
- if err != nil {
- logger.Error().Err(err)
- return MailboxesResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+func (j *Client) GetMailbox(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (map[string]MailboxesResponse, SessionState, Error) {
+ logger = j.logger("GetMailbox", session, logger)
+
+ uniqueAccountIds := structs.Uniq(accountIds)
+ if len(uniqueAccountIds) < 1 {
+ return map[string]MailboxesResponse{}, "", nil
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxesResponse, Error) {
- var response MailboxGetResponse
- err = retrieveResponseMatchParameters(body, CommandMailboxGet, "0", &response)
- if err != nil {
- logger.Error().Err(err)
- return MailboxesResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
- }
+ invocations := make([]Invocation, len(uniqueAccountIds))
+ for i, accountId := range uniqueAccountIds {
+ invocations[i] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, accountId)
+ }
- return MailboxesResponse{
- Mailboxes: response.List,
- NotFound: response.NotFound,
- State: response.State,
- }, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ cmd, err := request(invocations...)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxesResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]MailboxesResponse, Error) {
+ resp := map[string]MailboxesResponse{}
+ for _, accountId := range uniqueAccountIds {
+ var response MailboxGetResponse
+ err = retrieveResponseMatchParameters(body, CommandMailboxGet, "0", &response)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxesResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ }
+
+ resp[accountId] = MailboxesResponse{
+ Mailboxes: response.List,
+ NotFound: response.NotFound,
+ State: response.State,
+ }
+ }
+ return resp, nil
})
}
@@ -43,15 +59,21 @@ type AllMailboxesResponse struct {
State State `json:"state"`
}
-func (j *Client) GetAllMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger) (AllMailboxesResponse, SessionState, Error) {
- resp, sessionState, err := j.GetMailbox(accountId, session, ctx, logger, nil)
+func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (map[string]AllMailboxesResponse, SessionState, Error) {
+ resp, sessionState, err := j.GetMailbox(accountIds, session, ctx, logger, nil)
if err != nil {
- return AllMailboxesResponse{}, sessionState, err
+ return map[string]AllMailboxesResponse{}, sessionState, err
}
- return AllMailboxesResponse{
- Mailboxes: resp.Mailboxes,
- State: resp.State,
- }, sessionState, nil
+
+ mapped := make(map[string]AllMailboxesResponse, len(resp))
+ for accountId, mailboxesResponse := range resp {
+ mapped[accountId] = AllMailboxesResponse{
+ Mailboxes: mailboxesResponse.Mailboxes,
+ State: mailboxesResponse.State,
+ }
+ }
+
+ return mapped, sessionState, nil
}
type Mailboxes struct {
@@ -61,29 +83,41 @@ type Mailboxes struct {
State State `json:"state,omitempty"`
}
-func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (Mailboxes, SessionState, Error) {
- logger = j.logger(accountId, "SearchMailboxes", session, logger)
+func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (map[string]Mailboxes, SessionState, Error) {
+ logger = j.logger("SearchMailboxes", session, logger)
- cmd, err := request(
- invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: filter}, "0"),
- invocation(CommandMailboxGet, MailboxGetRefCommand{
+ uniqueAccountIds := structs.Uniq(accountIds)
+
+ invocations := make([]Invocation, len(uniqueAccountIds)*2)
+ for i, accountId := range uniqueAccountIds {
+ baseId := accountId + ":"
+ invocations[i*2+0] = invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: filter}, baseId+"0")
+ invocations[i*2+1] = invocation(CommandMailboxGet, MailboxGetRefCommand{
AccountId: accountId,
- IdRef: &ResultReference{Name: CommandMailboxQuery, Path: "/ids/*", ResultOf: "0"},
- }, "1"),
- )
+ IdRef: &ResultReference{Name: CommandMailboxQuery, Path: "/ids/*", ResultOf: baseId + "0"},
+ }, baseId+"1")
+ }
+ cmd, err := request(invocations...)
if err != nil {
logger.Error().Err(err)
- return Mailboxes{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ return map[string]Mailboxes{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Mailboxes, Error) {
- var response MailboxGetResponse
- err = retrieveResponseMatchParameters(body, CommandMailboxGet, "1", &response)
- if err != nil {
- logger.Error().Err(err)
- return Mailboxes{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]Mailboxes, Error) {
+ resp := map[string]Mailboxes{}
+ for _, accountId := range uniqueAccountIds {
+ baseId := accountId + ":"
+
+ var response MailboxGetResponse
+ err = retrieveResponseMatchParameters(body, CommandMailboxGet, baseId+"1", &response)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]Mailboxes{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ }
+
+ resp[accountId] = Mailboxes{Mailboxes: response.List, State: response.State}
}
- return Mailboxes{Mailboxes: response.List, State: response.State}, nil
+ return resp, nil
})
}
@@ -98,7 +132,7 @@ type MailboxChanges struct {
// Retrieve Email changes in a given Mailbox since a given state.
func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) {
- logger = j.loggerParams(accountId, "GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context {
+ logger = j.loggerParams("GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
@@ -169,3 +203,104 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte
}, nil
})
}
+
+// Retrieve Email changes in Mailboxes of multiple Accounts.
+func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, sinceStateMap map[string]string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (map[string]MailboxChanges, SessionState, Error) {
+ logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context {
+ sinceStateLogDict := zerolog.Dict()
+ for k, v := range sinceStateMap {
+ sinceStateLogDict.Str(log.SafeString(k), log.SafeString(v))
+ }
+ return z.Bool(logFetchBodies, fetchBodies).Dict(logSinceState, sinceStateLogDict)
+ })
+
+ uniqueAccountIds := structs.Uniq(accountIds)
+ n := len(uniqueAccountIds)
+ if n < 1 {
+ return map[string]MailboxChanges{}, "", nil
+ }
+
+ invocations := make([]Invocation, n*3)
+ for i, accountId := range uniqueAccountIds {
+ changes := MailboxChangesCommand{
+ AccountId: accountId,
+ }
+
+ sinceState, ok := sinceStateMap[accountId]
+ if ok {
+ changes.SinceState = sinceState
+ }
+
+ if maxChanges > 0 {
+ changes.MaxChanges = maxChanges
+ }
+
+ baseId := accountId + ":"
+
+ getCreated := EmailGetRefCommand{
+ AccountId: accountId,
+ FetchAllBodyValues: fetchBodies,
+ IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: baseId + "0"},
+ }
+ if maxBodyValueBytes > 0 {
+ getCreated.MaxBodyValueBytes = maxBodyValueBytes
+ }
+ getUpdated := EmailGetRefCommand{
+ AccountId: accountId,
+ FetchAllBodyValues: fetchBodies,
+ IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: baseId + "0"},
+ }
+ if maxBodyValueBytes > 0 {
+ getUpdated.MaxBodyValueBytes = maxBodyValueBytes
+ }
+
+ invocations[i*3+0] = invocation(CommandMailboxChanges, changes, baseId+"0")
+ invocations[i*3+1] = invocation(CommandEmailGet, getCreated, baseId+"1")
+ invocations[i*3+2] = invocation(CommandEmailGet, getUpdated, baseId+"2")
+ }
+
+ cmd, err := request(invocations...)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxChanges{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]MailboxChanges, Error) {
+ resp := make(map[string]MailboxChanges, n)
+ for _, accountId := range uniqueAccountIds {
+ baseId := accountId + ":"
+
+ var mailboxResponse MailboxChangesResponse
+ err = retrieveResponseMatchParameters(body, CommandMailboxChanges, baseId+"0", &mailboxResponse)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ }
+
+ var createdResponse EmailGetResponse
+ err = retrieveResponseMatchParameters(body, CommandEmailGet, baseId+"1", &createdResponse)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ }
+
+ var updatedResponse EmailGetResponse
+ err = retrieveResponseMatchParameters(body, CommandEmailGet, baseId+"2", &updatedResponse)
+ if err != nil {
+ logger.Error().Err(err)
+ return map[string]MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ }
+
+ resp[accountId] = MailboxChanges{
+ Destroyed: mailboxResponse.Destroyed,
+ HasMoreChanges: mailboxResponse.HasMoreChanges,
+ NewState: mailboxResponse.NewState,
+ Created: createdResponse.List,
+ Updated: createdResponse.List,
+ State: createdResponse.State,
+ }
+ }
+
+ return resp, nil
+ })
+}
diff --git a/pkg/jmap/jmap_api_vacation.go b/pkg/jmap/jmap_api_vacation.go
index 9c4dd15f0..f9639b296 100644
--- a/pkg/jmap/jmap_api_vacation.go
+++ b/pkg/jmap/jmap_api_vacation.go
@@ -14,7 +14,7 @@ const (
// https://jmap.io/spec-mail.html#vacationresponseget
func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, SessionState, Error) {
- logger = j.logger(accountId, "GetVacationResponse", session, logger)
+ logger = j.logger("GetVacationResponse", session, logger)
cmd, err := request(invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "0"))
if err != nil {
logger.Error().Err(err)
@@ -61,7 +61,7 @@ type VacationResponseChange struct {
}
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, SessionState, Error) {
- logger = j.logger(accountId, "SetVacationResponse", session, logger)
+ logger = j.logger("SetVacationResponse", session, logger)
cmd, err := request(
invocation(CommandVacationResponseSet, VacationResponseSetCommand{
diff --git a/pkg/jmap/jmap_client.go b/pkg/jmap/jmap_client.go
index f55db0661..1a1aa489a 100644
--- a/pkg/jmap/jmap_client.go
+++ b/pkg/jmap/jmap_client.go
@@ -50,14 +50,12 @@ func (j *Client) FetchSession(sessionUrl *url.URL, username string, logger *log.
return newSession(wk)
}
-func (j *Client) logger(accountId string, operation string, _ *Session, logger *log.Logger) *log.Logger {
- var _ string = accountId
+func (j *Client) logger(operation string, _ *Session, logger *log.Logger) *log.Logger {
l := logger.With().Str(logOperation, operation)
return log.From(l)
}
-func (j *Client) loggerParams(accountId string, operation string, _ *Session, logger *log.Logger, params func(zerolog.Context) zerolog.Context) *log.Logger {
- var _ string = accountId
+func (j *Client) loggerParams(operation string, _ *Session, logger *log.Logger, params func(zerolog.Context) zerolog.Context) *log.Logger {
l := logger.With().Str(logOperation, operation)
if params != nil {
l = params(l)
diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go
index cfebf3245..5bb9c0dfb 100644
--- a/pkg/jmap/jmap_integration_test.go
+++ b/pkg/jmap/jmap_integration_test.go
@@ -354,9 +354,13 @@ func TestWithStalwart(t *testing.T) {
var inboxFolder string
var inboxId string
{
- resp, sessionState, err := j.GetAllMailboxes(accountId, session, ctx, logger)
+ respByAccountId, sessionState, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger)
require.NoError(err)
require.Equal(session.State, sessionState)
+ require.Len(respByAccountId, 1)
+ require.Contains(respByAccountId, accountId)
+ resp := respByAccountId[accountId]
+
mailboxesNameByRole := map[string]string{}
mailboxesUnreadByRole := map[string]int{}
for _, m := range resp.Mailboxes {
@@ -436,9 +440,12 @@ func TestWithStalwart(t *testing.T) {
}
{
- resp, sessionState, err := j.GetAllMailboxes(accountId, session, ctx, logger)
+ respByAccountId, sessionState, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger)
require.NoError(err)
require.Equal(session.State, sessionState)
+ require.Len(respByAccountId, 1)
+ require.Contains(respByAccountId, accountId)
+ resp := respByAccountId[accountId]
mailboxesUnreadByRole := map[string]int{}
for _, m := range resp.Mailboxes {
if m.Role != "" {
diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go
index 960287781..5db86e73f 100644
--- a/pkg/jmap/jmap_test.go
+++ b/pkg/jmap/jmap_test.go
@@ -151,8 +151,11 @@ func TestRequests(t *testing.T) {
session := Session{Username: "user123", JmapUrl: *jmapUrl}
- folders, sessionState, err := client.GetAllMailboxes("a", &session, ctx, &logger)
+ foldersByAccountId, sessionState, err := client.GetAllMailboxes([]string{"a"}, &session, ctx, &logger)
require.NoError(err)
+ require.Len(foldersByAccountId, 1)
+ require.Contains(foldersByAccountId, "a")
+ folders := foldersByAccountId["a"]
require.Len(folders.Mailboxes, 5)
require.NotEmpty(sessionState)
diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go
index 5b86b9ec5..c343fb433 100644
--- a/services/groupware/pkg/groupware/groupware_api_mailbox.go
+++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go
@@ -4,9 +4,11 @@ import (
"net/http"
"github.com/go-chi/chi/v5"
+ "github.com/rs/zerolog"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/opencloud-eu/opencloud/pkg/structs"
)
// When the request succeeds.
@@ -39,13 +41,14 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
- res, sessionState, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, []string{mailboxId})
+ mailboxesByAccountId, sessionState, jerr := g.jmap.GetMailbox([]string{accountId}, req.session, req.ctx, req.logger, []string{mailboxId})
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(res.Mailboxes) == 1 {
- return etagResponse(res.Mailboxes[0], sessionState, res.State)
+ mailboxes, ok := mailboxesByAccountId[accountId]
+ if ok && len(mailboxes.Mailboxes) == 1 {
+ return etagResponse(mailboxes.Mailboxes[0], sessionState, mailboxes.State)
} else {
return notFoundResponse(sessionState)
}
@@ -74,7 +77,8 @@ type SwaggerMailboxesResponse200 struct {
}
// swagger:route GET /groupware/accounts/{account}/mailboxes mailbox mailboxes
-// Get the list of all the mailboxes of an account.
+// Get the list of all the mailboxes of an account, potentially filtering on the
+// name and/or role of the mailbox.
//
// A Mailbox represents a named set of Emails.
// This is the primary mechanism for organising Emails within an account.
@@ -120,17 +124,87 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
logger := log.From(req.logger.With().Str(logAccountId, accountId))
if hasCriteria {
- mailboxes, sessionState, err := g.jmap.SearchMailboxes(accountId, req.session, req.ctx, logger, filter)
+ mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes([]string{accountId}, req.session, req.ctx, logger, filter)
if err != nil {
return req.errorResponseFromJmap(err)
}
- return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
+
+ mailboxes, ok := mailboxesByAccountId[accountId]
+ if ok {
+ return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
+ } else {
+ return notFoundResponse(sessionState)
+ }
} else {
- mailboxes, sessionState, err := g.jmap.GetAllMailboxes(accountId, req.session, req.ctx, logger)
+ mailboxesByAccountId, sessionState, err := g.jmap.GetAllMailboxes([]string{accountId}, req.session, req.ctx, logger)
if err != nil {
return req.errorResponseFromJmap(err)
}
- return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
+ mailboxes, ok := mailboxesByAccountId[accountId]
+ if ok {
+ return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
+ } else {
+ return notFoundResponse(sessionState)
+ }
+ }
+ })
+}
+
+// When the request succeeds.
+// swagger:response MailboxesForAllAccountsResponse200
+type SwaggerMailboxesForAllAccountsResponse200 struct {
+ // in: body
+ Body map[string][]jmap.Mailbox
+}
+
+// swagger:route GET /groupware/accounts/all/mailboxes mailbox mailboxesforallaccounts
+// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the
+// role of the mailboxes.
+//
+// responses:
+//
+// 200: MailboxesForAllAccountsResponse200
+// 400: ErrorResponse400
+// 500: ErrorResponse500
+func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) {
+ q := r.URL.Query()
+ var filter jmap.MailboxFilterCondition
+
+ hasCriteria := false
+ role := q.Get(QueryParamMailboxSearchRole)
+ if role != "" {
+ filter.Role = role
+ hasCriteria = true
+ }
+
+ g.respond(w, r, func(req Request) Response {
+ subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false)
+ if err != nil {
+ return errorResponse(err)
+ }
+ if set {
+ filter.IsSubscribed = &subscribed
+ hasCriteria = true
+ }
+
+ accountIds := structs.Keys(req.session.Accounts)
+ if len(accountIds) < 1 {
+ return noContentResponse("")
+ }
+ logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
+
+ if hasCriteria {
+ mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, filter)
+ if err != nil {
+ return req.errorResponseFromJmap(err)
+ }
+ return response(mailboxesByAccountId, sessionState)
+ } else {
+ mailboxesByAccountId, sessionState, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger)
+ if err != nil {
+ return req.errorResponseFromJmap(err)
+ }
+ return response(mailboxesByAccountId, sessionState)
}
})
}
@@ -181,3 +255,56 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
return etagResponse(changes, sessionState, changes.State)
})
}
+
+// When the request succeeds.
+// swagger:response MailboxChangesForAllAccountsResponse200
+type SwaggerMailboxChangesForAllAccountsResponse200 struct {
+ // in: body
+ Body map[string]jmap.MailboxChanges
+}
+
+// swagger:route GET /groupware/accounts/all/mailboxes/changes mailbox mailboxchangesforallaccounts
+// Get the changes that occured in all the mailboxes of all accounts.
+//
+// responses:
+//
+// 200: MailboxChangesForAllAccountsResponse200
+// 400: ErrorResponse400
+// 500: ErrorResponse500
+func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With()
+
+ sinceStateMap, ok, err := req.parseMapParam(QueryParamSince)
+ if err != nil {
+ return errorResponse(err)
+ }
+ if ok {
+ dict := zerolog.Dict()
+ for k, v := range sinceStateMap {
+ dict.Str(log.SafeString(k), log.SafeString(v))
+ }
+ l = l.Dict(QueryParamSince, dict)
+ }
+
+ maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
+ if err != nil {
+ return errorResponse(err)
+ }
+ if ok {
+ l = l.Uint(QueryParamMaxChanges, maxChanges)
+ }
+
+ allAccountIds := structs.Keys(req.session.Accounts) // TODO(pbleser-oc) do we need a limit for a maximum amount of accounts to query at once?
+ l.Array(logAccountId, log.SafeStringArray(allAccountIds))
+
+ logger := log.From(l)
+
+ changesByAccountId, sessionState, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, sinceStateMap, true, g.maxBodyValueBytes, maxChanges)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return response(changesByAccountId, sessionState)
+ })
+}
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index cbe01b6e2..625538cbd 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"strconv"
+ "strings"
"time"
"github.com/go-chi/chi/v5"
@@ -224,6 +225,25 @@ func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *E
return b, true, nil
}
+func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
+ q := r.r.URL.Query()
+ if !q.Has(param) {
+ return map[string]string{}, false, nil
+ }
+
+ result := map[string]string{}
+ prefix := param + "."
+ for name, values := range q {
+ if strings.HasPrefix(name, prefix) {
+ if len(values) > 0 {
+ key := name[len(prefix)+1:]
+ result[key] = values[0]
+ }
+ }
+ }
+ return result, true, nil
+}
+
func (r Request) body(target any) *Error {
body := r.r.Body
defer func(b io.ReadCloser) {
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index c33382f96..895ac58f0 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -1,11 +1,13 @@
package groupware
import (
+ "net/http"
+
"github.com/go-chi/chi/v5"
)
const (
- defaultAccountId = "*"
+ defaultAccountId = "_"
UriParamAccountId = "accountid"
UriParamMailboxId = "mailbox"
@@ -42,10 +44,14 @@ const (
)
func (g *Groupware) Route(r chi.Router) {
- r.HandleFunc("/events/{stream}", g.ServeSSE)
-
r.Get("/", g.Index)
r.Get("/accounts", g.GetAccounts)
+ r.Route("/accounts/all", func(r chi.Router) {
+ r.Route("/mailboxes", func(r chi.Router) {
+ r.Get("/", g.GetMailboxesForAllAccounts)
+ r.Get("/changes", g.GetMailboxChangesForAllAccounts)
+ })
+ })
r.Route("/accounts/{accountid}", func(r chi.Router) {
r.Get("/", g.GetAccount)
r.Get("/bootstrap", g.GetAccountBootstrap)
@@ -65,7 +71,7 @@ func (g *Groupware) Route(r chi.Router) {
// r.Put("/{messageid}", g.ReplaceMessage) // TODO
r.Patch("/{messageid}", g.UpdateMessage)
r.Delete("/{messageid}", g.DeleteMessage)
- r.MethodFunc("REPORT", "/{messageid}", g.RelatedToMessage)
+ Report(r, "/{messageid}", g.RelatedToMessage)
})
r.Route("/blobs", func(r chi.Router) {
r.Get("/{blobid}", g.GetBlob)
@@ -73,6 +79,12 @@ func (g *Groupware) Route(r chi.Router) {
})
})
+ r.HandleFunc("/events/{stream}", g.ServeSSE)
+
r.NotFound(g.NotFound)
r.MethodNotAllowed(g.MethodNotAllowed)
}
+
+func Report(r chi.Router, pattern string, h http.HandlerFunc) {
+ r.MethodFunc("REPORT", pattern, h)
+}