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