From 0202bb43afb2183b12bd4c2b86b70b2db65b3193 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 24 Mar 2026 09:57:45 +0100 Subject: [PATCH] groupware: refactor contactcard changes, and Request framework * implement ContactCard retrieval endpoint for syncing * re-implement that endpoint for Email too * fix the Mailbox changes endpoint to actually return changes about Mailboxes, and not about Emails * when querying the diff of Mailboxes without any prior state, return an error since the result is not what one would expect * introduce the 'changes' API tag and group * refactor the successful response functions to consistently return an object type and object state whenever possible * move the syncing endpoints under /accounts/*/changes/ for better clarity, e.g. /changes/emails instead of /emails/mailbox/*/changes --- pkg/jmap/api_contact.go | 97 ++++++ pkg/jmap/api_email.go | 53 +-- pkg/jmap/api_mailbox.go | 94 +++--- pkg/jmap/integration_ws_test.go | 14 +- pkg/jmap/model.go | 51 ++- pkg/jmap/model_examples.go | 36 ++- services/groupware/apidoc.yml | 6 + .../groupware/pkg/groupware/api_account.go | 10 +- services/groupware/pkg/groupware/api_blob.go | 16 +- .../groupware/pkg/groupware/api_calendars.go | 50 +-- .../groupware/pkg/groupware/api_contacts.go | 112 +++++-- .../groupware/pkg/groupware/api_emails.go | 303 +++++++++--------- .../groupware/pkg/groupware/api_identity.go | 44 +-- services/groupware/pkg/groupware/api_index.go | 4 +- .../groupware/pkg/groupware/api_mailbox.go | 126 ++++---- services/groupware/pkg/groupware/api_quota.go | 14 +- .../groupware/pkg/groupware/api_tasklists.go | 14 +- .../groupware/pkg/groupware/api_vacation.go | 14 +- services/groupware/pkg/groupware/error.go | 20 +- services/groupware/pkg/groupware/request.go | 28 +- services/groupware/pkg/groupware/response.go | 83 +++-- services/groupware/pkg/groupware/route.go | 9 +- 22 files changed, 732 insertions(+), 466 deletions(-) diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index a09584fbf4..790fbcb24d 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -63,6 +63,103 @@ func (j *Client) GetContactCardsById(accountId string, session *Session, ctx con }) } +func (j *Client) GetContactCards(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, contactIds []string) ([]jscontact.ContactCard, SessionState, State, Language, Error) { + logger = j.logger("GetContactCards", session, logger) + + cmd, err := j.request(session, logger, invocation(CommandContactCardGet, ContactCardGetCommand{ + Ids: contactIds, + AccountId: accountId, + }, "0")) + if err != nil { + return nil, "", "", "", err + } + + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]jscontact.ContactCard, State, Error) { + var response ContactCardGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "0", &response) + if err != nil { + return nil, "", err + } + return response.List, response.State, nil + }) +} + +type ContactCardChanges struct { + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + HasMoreChanges bool `json:"hasMoreChanges"` + Created []jscontact.ContactCard `json:"created,omitempty"` + Updated []jscontact.ContactCard `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` +} + +func (j *Client) GetContactCardsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, sinceState string, maxChanges uint) (ContactCardChanges, SessionState, State, Language, Error) { + logger = j.logger("GetContactCards", session, logger) + + maxChangesPtr := &maxChanges + if maxChanges < 1 { + maxChangesPtr = nil + } + + cmd, err := j.request(session, logger, + invocation(CommandContactCardChanges, ContactCardChangesCommand{ + AccountId: accountId, + SinceState: sinceState, + MaxChanges: maxChangesPtr, + }, "0"), + invocation(CommandContactCardGet, ContactCardGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + ResultOf: "0", + Name: CommandContactCardChanges, + Path: "/created", + }, + }, "1"), + invocation(CommandContactCardGet, ContactCardGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + ResultOf: "0", + Name: CommandContactCardChanges, + Path: "/updated", + }, + }, "2"), + ) + if err != nil { + return ContactCardChanges{}, "", "", "", err + } + + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (ContactCardChanges, State, Error) { + result := ContactCardChanges{} + var changes ContactCardChangesResponse + err = retrieveResponseMatchParameters(logger, body, CommandContactCardChanges, "0", &changes) + if err != nil { + return ContactCardChanges{}, "", err + } + result.NewState = changes.NewState + result.OldState = changes.OldState + result.HasMoreChanges = changes.HasMoreChanges + result.Destroyed = changes.Destroyed + + var created ContactCardGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "1", &created) + if err != nil { + return ContactCardChanges{}, "", err + } + result.Created = created.List + + var updated ContactCardGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "2", &updated) + if err != nil { + return ContactCardChanges{}, "", err + } + result.Updated = updated.List + + return result, changes.NewState, nil + }) +} + func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) { diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index c728dead77..e6cffd3ef5 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -194,38 +194,18 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c }) } -func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (EmailChangesResponse, SessionState, State, Language, Error) { - logger = j.loggerParams("GetEmailChanges", session, logger, func(z zerolog.Context) zerolog.Context { - return z.Str(logSinceState, string(sinceState)) - }) - - changes := EmailChangesCommand{ - AccountId: accountId, - SinceState: sinceState, - } - if maxChanges > 0 { - changes.MaxChanges = maxChanges - } - - cmd, err := j.request(session, logger, invocation(CommandEmailChanges, changes, "0")) - if err != nil { - return EmailChangesResponse{}, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) - } - - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailChangesResponse, State, Error) { - var changesResponse EmailChangesResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailChanges, "0", &changesResponse) - if err != nil { - return EmailChangesResponse{}, "", err - } - - return changesResponse, changesResponse.NewState, nil - }) +type EmailChanges struct { + HasMoreChanges bool `json:"hasMoreChanges"` + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + Created []Email `json:"created,omitempty"` + Updated []Email `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` } // 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, acceptLanguage string, sinceState State, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) { - logger = j.loggerParams("GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context { +func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailChanges, SessionState, State, Language, Error) { + logger = j.loggerParams("GetEmailChanges", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, string(sinceState)) }) @@ -234,7 +214,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context. SinceState: sinceState, } if maxChanges > 0 { - changes.MaxChanges = maxChanges + changes.MaxChanges = &maxChanges } getCreated := EmailGetRefCommand{ @@ -260,33 +240,34 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context. invocation(CommandEmailGet, getUpdated, "2"), ) if err != nil { - return MailboxChanges{}, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) + return EmailChanges{}, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, State, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailChanges, State, Error) { var changesResponse EmailChangesResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailChanges, "0", &changesResponse) if err != nil { - return MailboxChanges{}, "", err + return EmailChanges{}, "", err } var createdResponse EmailGetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &createdResponse) if err != nil { logger.Error().Err(err).Send() - return MailboxChanges{}, "", err + return EmailChanges{}, "", err } var updatedResponse EmailGetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "2", &updatedResponse) if err != nil { logger.Error().Err(err).Send() - return MailboxChanges{}, "", err + return EmailChanges{}, "", err } - return MailboxChanges{ + return EmailChanges{ Destroyed: changesResponse.Destroyed, HasMoreChanges: changesResponse.HasMoreChanges, + OldState: changesResponse.OldState, NewState: changesResponse.NewState, Created: createdResponse.List, Updated: updatedResponse.List, diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 4b18829e11..0c0b2b643e 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -155,48 +155,41 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, } type MailboxChanges struct { - Destroyed []string `json:"destroyed,omitzero"` - HasMoreChanges bool `json:"hasMoreChanges,omitzero"` - NewState State `json:"newState"` - Created []Email `json:"created,omitempty"` - Updated []Email `json:"updated,omitempty"` + HasMoreChanges bool `json:"hasMoreChanges"` + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + Created []Mailbox `json:"created,omitempty"` + Updated []Mailbox `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` } -// 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, acceptLanguage string, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) { - logger = j.loggerParams("GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context { - return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState) - }) +// Retrieve Mailbox changes since a given state. +// @apidoc mailboxes,changes +func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState string, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) { + logger = j.logger("GetMailboxChanges", session, logger) changes := MailboxChangesCommand{ AccountId: accountId, SinceState: sinceState, + MaxChanges: nil, } if maxChanges > 0 { - changes.MaxChanges = maxChanges + changes.MaxChanges = &maxChanges } - getCreated := EmailGetRefCommand{ - AccountId: accountId, - FetchAllBodyValues: fetchBodies, - IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"}, + getCreated := MailboxGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"}, } - if maxBodyValueBytes > 0 { - getCreated.MaxBodyValueBytes = maxBodyValueBytes - } - getUpdated := EmailGetRefCommand{ - AccountId: accountId, - FetchAllBodyValues: fetchBodies, - IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"}, - } - if maxBodyValueBytes > 0 { - getUpdated.MaxBodyValueBytes = maxBodyValueBytes + getUpdated := MailboxGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"}, } cmd, err := j.request(session, logger, invocation(CommandMailboxChanges, changes, "0"), - invocation(CommandEmailGet, getCreated, "1"), - invocation(CommandEmailGet, getUpdated, "2"), + invocation(CommandMailboxGet, getCreated, "1"), + invocation(CommandMailboxGet, getUpdated, "2"), ) if err != nil { return MailboxChanges{}, "", "", "", err @@ -209,15 +202,15 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte return MailboxChanges{}, "", err } - var createdResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &createdResponse) + var createdResponse MailboxGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "1", &createdResponse) if err != nil { logger.Error().Err(err).Send() return MailboxChanges{}, "", err } - var updatedResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "2", &updatedResponse) + var updatedResponse MailboxGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "2", &updatedResponse) if err != nil { logger.Error().Err(err).Send() return MailboxChanges{}, "", err @@ -226,6 +219,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte return MailboxChanges{ Destroyed: mailboxResponse.Destroyed, HasMoreChanges: mailboxResponse.HasMoreChanges, + OldState: mailboxResponse.OldState, NewState: mailboxResponse.NewState, Created: createdResponse.List, Updated: createdResponse.List, @@ -234,13 +228,13 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte } // Retrieve Email changes in Mailboxes of multiple Accounts. -func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, Error) { +func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, 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) + return z.Dict(logSinceState, sinceStateLogDict) }) uniqueAccountIds := structs.Uniq(accountIds) @@ -261,29 +255,21 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi } if maxChanges > 0 { - changes.MaxChanges = maxChanges + changes.MaxChanges = &maxChanges } - getCreated := EmailGetRefCommand{ - AccountId: accountId, - FetchAllBodyValues: fetchBodies, - IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")}, + getCreated := MailboxGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")}, } - if maxBodyValueBytes > 0 { - getCreated.MaxBodyValueBytes = maxBodyValueBytes - } - getUpdated := EmailGetRefCommand{ - AccountId: accountId, - FetchAllBodyValues: fetchBodies, - IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")}, - } - if maxBodyValueBytes > 0 { - getUpdated.MaxBodyValueBytes = maxBodyValueBytes + getUpdated := MailboxGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")}, } invocations[i*3+0] = invocation(CommandMailboxChanges, changes, mcid(accountId, "0")) - invocations[i*3+1] = invocation(CommandEmailGet, getCreated, mcid(accountId, "1")) - invocations[i*3+2] = invocation(CommandEmailGet, getUpdated, mcid(accountId, "2")) + invocations[i*3+1] = invocation(CommandMailboxGet, getCreated, mcid(accountId, "1")) + invocations[i*3+2] = invocation(CommandMailboxGet, getUpdated, mcid(accountId, "2")) } cmd, err := j.request(session, logger, invocations...) @@ -301,14 +287,14 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi return nil, "", err } - var createdResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &createdResponse) + var createdResponse MailboxGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &createdResponse) if err != nil { return nil, "", err } - var updatedResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "2"), &updatedResponse) + var updatedResponse MailboxGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "2"), &updatedResponse) if err != nil { return nil, "", err } diff --git a/pkg/jmap/integration_ws_test.go b/pkg/jmap/integration_ws_test.go index 065e9827ae..f421413966 100644 --- a/pkg/jmap/integration_ws_test.go +++ b/pkg/jmap/integration_ws_test.go @@ -90,7 +90,7 @@ func TestWs(t *testing.T) { var initialState State { - changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", "", 0) + changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", State(""), true, 0, 0) require.NoError(err) require.Equal(session.State, sessionState) require.NotEmpty(state) @@ -104,7 +104,7 @@ func TestWs(t *testing.T) { require.NotEmpty(initialState) { - changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, 0) + changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, true, 0, 0) require.NoError(err) require.Equal(session.State, sessionState) require.Equal(initialState, state) @@ -147,7 +147,7 @@ func TestWs(t *testing.T) { } var lastState State { - changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, 0) + changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, true, 0, 0) require.NoError(err) require.Equal(session.State, sessionState) require.NotEqual(initialState, state) @@ -158,7 +158,7 @@ func TestWs(t *testing.T) { require.Empty(changes.Updated) lastState = state - emailIds = append(emailIds, changes.Created...) + emailIds = append(emailIds, structs.Map(changes.Created, func(e Email) string { return e.Id })...) } { @@ -181,7 +181,7 @@ func TestWs(t *testing.T) { l.m.Unlock() } { - changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, 0) + changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, true, 0, 0) require.NoError(err) require.Equal(session.State, sessionState) require.NotEqual(lastState, state) @@ -192,7 +192,7 @@ func TestWs(t *testing.T) { require.Empty(changes.Updated) lastState = state - emailIds = append(emailIds, changes.Created...) + emailIds = append(emailIds, structs.Map(changes.Created, func(e Email) string { return e.Id })...) } { @@ -215,7 +215,7 @@ func TestWs(t *testing.T) { l.m.Unlock() } { - changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, 0) + changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, true, 0, 0) require.NoError(err) require.Equal(session.State, sessionState) require.NotEqual(lastState, state) diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 54712fa86d..6ed66897c4 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -842,8 +842,12 @@ type SessionState string type State string +const EmptyState = State("") + type Language string +const NoLanguage = Language("") + type SessionResponse struct { Capabilities SessionCapabilities `json:"capabilities"` @@ -1350,7 +1354,7 @@ type MailboxChangesCommand struct { // If supplied by the client, the value MUST be a positive integer greater than 0. // // If a value outside of this range is given, the server MUST reject the call with an invalidArguments error. - MaxChanges uint `json:"maxChanges,omitzero"` + MaxChanges *uint `json:"maxChanges,omitzero"` } type MailboxFilterElement interface { @@ -1859,7 +1863,7 @@ type EmailChangesCommand struct { // The server MAY choose to return fewer than this value but MUST NOT return more. // If not given by the client, the server may choose how many to return. // If supplied by the client, the value MUST be a positive integer greater than 0. - MaxChanges uint `json:"maxChanges,omitzero"` + MaxChanges *uint `json:"maxChanges,omitempty"` } type EmailAddress struct { @@ -5307,6 +5311,47 @@ type ContactCardGetResponse struct { NotFound []any `json:"notFound"` } +type ContactCardChangesCommand struct { + // The id of the account to use. + AccountId string `json:"accountId"` + + // The current state of the client. + // This is the string that was returned as the "state" argument in the "ContactCard/get" response. + // The server will return the changes that have occurred since this state. + SinceState string `json:"sinceState,omitempty"` + + // The maximum number of ids to return in the response. + // The server MAY choose to return fewer than this value but MUST NOT return more. + // If not given by the client, the server may choose how many to return. + // If supplied by the client, the value MUST be a positive integer greater than 0. + // If a value outside of this range is given, the server MUST reject the call with an `invalidArguments` error. + MaxChanges *uint `json:"maxChanges,omitempty"` +} + +type ContactCardChangesResponse struct { + // The id of the account used for the call. + AccountId string `json:"accountId"` + + // This is the "sinceState" argument echoed back; it's the state from which the server is returning changes. + OldState State `json:"oldState"` + + // This is the state the client will be in after applying the set of changes to the old state. + NewState State `json:"newState"` + + // If true, the client may call "ContactCard/changes" again with the "newState" returned to get further updates. + // If false, "newState" is the current server state. + HasMoreChanges bool `json:"hasMoreChanges"` + + // An array of ids for records that have been created since the old state. + Created []string `json:"created,omitempty"` + + // An array of ids for records that have been updated since the old state. + Updated []string `json:"updated,omitempty"` + + // An array of ids for records that have been destroyed since the old state. + Destroyed []string `json:"destroyed,omitempty"` +} + type ContactCardUpdate map[string]any type ContactCardSetCommand struct { @@ -5884,6 +5929,7 @@ const ( CommandAddressBookGet Command = "AddressBook/get" CommandContactCardQuery Command = "ContactCard/query" CommandContactCardGet Command = "ContactCard/get" + CommandContactCardChanges Command = "ContactCard/changes" CommandContactCardSet Command = "ContactCard/set" CommandCalendarEventParse Command = "CalendarEvent/parse" CommandCalendarGet Command = "Calendar/get" @@ -5916,6 +5962,7 @@ var CommandResponseTypeMap = map[Command]func() any{ CommandAddressBookGet: func() any { return AddressBookGetResponse{} }, CommandContactCardQuery: func() any { return ContactCardQueryResponse{} }, CommandContactCardGet: func() any { return ContactCardGetResponse{} }, + CommandContactCardChanges: func() any { return ContactCardChangesResponse{} }, CommandContactCardSet: func() any { return ContactCardSetResponse{} }, CommandCalendarEventParse: func() any { return CalendarEventParseResponse{} }, CommandCalendarGet: func() any { return CalendarGetResponse{} }, diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index f7db621bf1..933b1441b3 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/opencloud-eu/opencloud/pkg/jscontact" c "github.com/opencloud-eu/opencloud/pkg/jscontact" ) @@ -697,9 +698,10 @@ func (e Exemplar) Mailboxes() []Mailbox { } func (e Exemplar) MailboxChanges() MailboxChanges { + a, _, _ := e.MailboxInbox() return MailboxChanges{ NewState: "aesh2ahj", - Created: []Email{e.Email()}, + Created: []Mailbox{a}, Destroyed: []string{"baingow4"}, } } @@ -1783,3 +1785,35 @@ func (e Exemplar) ContactCard() c.ContactCard { }, } } + +func (e Exemplar) ContactCardChanges() (ContactCardChanges, string, string) { + c := e.ContactCard() + return ContactCardChanges{ + OldState: "xai3iiraipoo", + NewState: "ni7thah7eeY4", + HasMoreChanges: true, + Created: []jscontact.ContactCard{c}, + Destroyed: []string{"eaae", "bcba"}, + }, "A created ContactCard and two deleted ones", "created" +} + +func (e Exemplar) OtherContactCardChanges() (ContactCardChanges, string, string) { + c := e.ContactCard() + return ContactCardChanges{ + OldState: "xai3iiraipoo", + NewState: "ni7thah7eeY4", + HasMoreChanges: false, + Updated: []jscontact.ContactCard{c}, + }, "An updated ContactCard", "updated" +} + +func (e Exemplar) EmailChanges() EmailChanges { + emails := []Email{e.Email()} + return EmailChanges{ + OldState: "xai3iiraipoo", + NewState: "ni7thah7eeY4", + HasMoreChanges: true, + Created: emails, + Destroyed: []string{"mmnan", "moxzz"}, + } +} diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml index d268807018..09f71ff7b0 100644 --- a/services/groupware/apidoc.yml +++ b/services/groupware/apidoc.yml @@ -58,6 +58,9 @@ tags: - name: vacation x-displayName: Vacation Responses description: APIs about vacation responses + - name: changes + x-displayName: Changes + description: APIs for retrieving changes to objects x-tagGroups: - name: Bootstrapping tags: @@ -86,6 +89,9 @@ x-tagGroups: - name: Quotas tags: - quota + - name: changes + tags: + - changes - name: Uncategorized tags: - untagged diff --git a/services/groupware/pkg/groupware/api_account.go b/services/groupware/pkg/groupware/api_account.go index 42f922737a..1f24181ff2 100644 --- a/services/groupware/pkg/groupware/api_account.go +++ b/services/groupware/pkg/groupware/api_account.go @@ -14,10 +14,10 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, account, err := req.GetAccountForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } var body jmap.Account = account - return etagResponse(single(accountId), body, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "") + return req.respond(accountId, body, req.session.State, AccountResponseObjectType, "") }) } @@ -36,7 +36,7 @@ func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) { // sort on accountId to have a stable order that remains the same with every query slices.SortFunc(list, func(a, b AccountWithId) int { return strings.Compare(a.AccountId, b.AccountId) }) var RBODY []AccountWithId = list - return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "") + return req.respondN(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, "") }) } @@ -46,7 +46,7 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt allAccountIds := req.AllAccountIds() resp, sessionState, state, lang, err := g.jmap.GetIdentitiesForAllAccounts(allAccountIds, req.session, req.ctx, req.logger, req.language()) if err != nil { - return req.errorResponseFromJmap(allAccountIds, err) + return req.jmapErrorN(allAccountIds, err, sessionState, lang) } list := make([]AccountWithIdAndIdentities, len(req.session.Accounts)) i := 0 @@ -66,7 +66,7 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt // sort on accountId to have a stable order that remains the same with every query slices.SortFunc(list, func(a, b AccountWithIdAndIdentities) int { return strings.Compare(a.AccountId, b.AccountId) }) var RBODY []AccountWithIdAndIdentities = list - return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang) + return req.respondN(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_blob.go b/services/groupware/pkg/groupware/api_blob.go index e4975825bc..bf41842e48 100644 --- a/services/groupware/pkg/groupware/api_blob.go +++ b/services/groupware/pkg/groupware/api_blob.go @@ -16,13 +16,13 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForBlob() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l := req.logger.With().Str(logAccountId, accountId) blobId, err := req.PathParam(UriParamBlobId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamBlobId, blobId) @@ -30,12 +30,12 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) { res, sessionState, state, lang, jerr := g.jmap.GetBlobMetadata(accountId, req.session, req.ctx, logger, req.language(), blobId) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if res == nil { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, BlobResponseObjectType, state) } - return etagResponse(single(accountId), res, sessionState, BlobResponseObjectType, state, lang) + return req.respond(accountId, res, sessionState, BlobResponseObjectType, state) }) } @@ -54,16 +54,16 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) { accountId, err := req.GetAccountIdForBlob() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) resp, lang, jerr := g.jmap.UploadBlobStream(accountId, req.session, req.ctx, logger, req.language(), contentType, body) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, req.session.State, lang) } - return response(single(accountId), resp, req.session.State, lang) + return req.respondWithoutStatus(accountId, resp) }) } diff --git a/services/groupware/pkg/groupware/api_calendars.go b/services/groupware/pkg/groupware/api_calendars.go index 585ff7e101..21c62919d4 100644 --- a/services/groupware/pkg/groupware/api_calendars.go +++ b/services/groupware/pkg/groupware/api_calendars.go @@ -18,10 +18,10 @@ func (g *Groupware) GetCalendars(w http.ResponseWriter, r *http.Request) { calendars, sessionState, state, lang, jerr := g.jmap.GetCalendars(accountId, req.session, req.ctx, req.logger, req.language(), nil) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), calendars, sessionState, CalendarResponseObjectType, state, lang) + return req.respond(accountId, calendars, sessionState, CalendarResponseObjectType, state) }) } @@ -37,20 +37,20 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { calendarId, err := req.PathParam(UriParamCalendarId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamCalendarId, log.SafeString(calendarId)) logger := log.From(l) calendars, sessionState, state, lang, jerr := g.jmap.GetCalendars(accountId, req.session, req.ctx, logger, req.language(), []string{calendarId}) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(calendars.NotFound) > 0 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, CalendarResponseObjectType, state) } else { - return etagResponse(single(accountId), calendars.Calendars[0], sessionState, CalendarResponseObjectType, state, lang) + return req.respond(accountId, calendars.Calendars[0], sessionState, CalendarResponseObjectType, state) } }) } @@ -67,13 +67,13 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) calendarId, err := req.PathParam(UriParamCalendarId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamCalendarId, log.SafeString(calendarId)) offset, ok, err := req.parseUIntParam(QueryParamOffset, 0) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint(QueryParamOffset, offset) @@ -81,7 +81,7 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint(QueryParamLimit, limit) @@ -95,13 +95,13 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) logger := log.From(l) eventsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryCalendarEvents(single(accountId), req.session, req.ctx, logger, req.language(), filter, sortBy, offset, limit) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if events, ok := eventsByAccountId[accountId]; ok { - return etagResponse(single(accountId), events, sessionState, EventResponseObjectType, state, lang) + return req.respond(accountId, events, sessionState, EventResponseObjectType, state) } else { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EventResponseObjectType, state) } }) } @@ -118,15 +118,15 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request) var create jmap.CalendarEvent err := req.body(&create) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(l) created, sessionState, state, lang, jerr := g.jmap.CreateCalendarEvent(accountId, req.session, req.ctx, logger, req.language(), create) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, EventResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, EventResponseObjectType, state) }) } @@ -140,33 +140,33 @@ func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) eventId, err := req.PathParam(UriParamEventId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEventId, log.SafeString(eventId)) logger := log.From(l) - deleted, sessionState, state, _, jerr := g.jmap.DeleteCalendarEvent(accountId, []string{eventId}, req.session, req.ctx, logger, req.language()) + deleted, sessionState, state, lang, jerr := g.jmap.DeleteCalendarEvent(accountId, []string{eventId}, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } for _, e := range deleted { desc := e.Description if desc != "" { - return errorResponseWithSessionState(single(accountId), apiError( + return req.errorS(accountId, apiError( req.errorId(), ErrorFailedToDeleteContact, withDetail(e.Description), ), sessionState) } else { - return errorResponseWithSessionState(single(accountId), apiError( + return req.errorS(accountId, apiError( req.errorId(), ErrorFailedToDeleteContact, ), sessionState) } } - return noContentResponseWithEtag(single(accountId), sessionState, EventResponseObjectType, state) + return req.noContent(accountId, sessionState, EventResponseObjectType, state) }) } @@ -174,12 +174,12 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForBlob() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } blobId, err := req.PathParam(UriParamBlobId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } blobIds := strings.Split(blobId, ",") @@ -188,8 +188,8 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) { resp, sessionState, state, lang, jerr := g.jmap.ParseICalendarBlob(accountId, req.session, req.ctx, logger, req.language(), blobIds) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), resp, sessionState, EventResponseObjectType, state, lang) + return req.respond(accountId, resp, sessionState, EventResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go index fa8a2ed8ea..28e9081642 100644 --- a/services/groupware/pkg/groupware/api_contacts.go +++ b/services/groupware/pkg/groupware/api_contacts.go @@ -51,11 +51,11 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) { addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, req.logger, req.language(), nil) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } var body jmap.AddressBooksResponse = addressbooks - return etagResponse(single(accountId), body, sessionState, AddressBookResponseObjectType, state, lang) + return req.respond(accountId, body, sessionState, AddressBookResponseObjectType, state) }) } @@ -71,20 +71,20 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) { addressBookId, err := req.PathParam(UriParamAddressBookId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) logger := log.From(l) addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, logger, req.language(), []string{addressBookId}) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(addressbooks.NotFound) > 0 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, AddressBookResponseObjectType, state) } else { - return etagResponse(single(accountId), addressbooks.AddressBooks[0], sessionState, AddressBookResponseObjectType, state, lang) + return req.respond(accountId, addressbooks.AddressBooks[0], sessionState, AddressBookResponseObjectType, state) } }) } @@ -102,13 +102,13 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ addressBookId, err := req.PathParam(UriParamAddressBookId) if err != nil { - return errorResponseWithSessionState(accountIds, err, req.session.State) + return req.errorN(accountIds, err) } l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) offset, ok, err := req.parseUIntParam(QueryParamOffset, 0) if err != nil { - return errorResponseWithSessionState(accountIds, err, req.session.State) + return req.errorN(accountIds, err) } if ok { l = l.Uint(QueryParamOffset, offset) @@ -116,7 +116,7 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) if err != nil { - return errorResponseWithSessionState(accountIds, err, req.session.State) + return req.errorN(accountIds, err) } if ok { l = l.Uint(QueryParamLimit, limit) @@ -135,13 +135,13 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ logger := log.From(l) contactsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryContactCards(accountIds, req.session, req.ctx, logger, req.language(), filter, sortBy, offset, limit) if jerr != nil { - return req.errorResponseFromJmap(accountIds, jerr) + return req.jmapErrorN(accountIds, jerr, sessionState, lang) } if contacts, ok := contactsByAccountId[accountId]; ok { - return etagResponse(accountIds, contacts, sessionState, ContactResponseObjectType, state, lang) + return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state) } else { - return etagNotFoundResponse(accountIds, sessionState, ContactResponseObjectType, state, lang) + return req.notFoundN(accountIds, sessionState, ContactResponseObjectType, state) } }) } @@ -157,24 +157,80 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) { contactId, err := req.PathParam(UriParamContactId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamContactId, log.SafeString(contactId)) logger := log.From(l) contactsById, sessionState, state, lang, jerr := g.jmap.GetContactCardsById(accountId, req.session, req.ctx, logger, req.language(), []string{contactId}) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if contact, ok := contactsById[contactId]; ok { - return etagResponse(single(accountId), contact, sessionState, ContactResponseObjectType, state, lang) + return req.respond(accountId, contact, sessionState, ContactResponseObjectType, state) } else { - return etagNotFoundResponse(single(accountId), sessionState, ContactResponseObjectType, state, lang) + return req.notFound(accountId, sessionState, ContactResponseObjectType, state) } }) } +func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + ok, accountId, resp := req.needContactWithAccount() + if !ok { + return resp + } + + l := req.logger.With() + + logger := log.From(l) + contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, req.session, req.ctx, logger, req.language(), []string{}) + if jerr != nil { + return req.jmapError(accountId, jerr, sessionState, lang) + } + var body []jscontact.ContactCard = contacts + + return req.respond(accountId, body, sessionState, ContactResponseObjectType, state) + }) +} + +// Get changes to Contacts since a given State +// @api:tags contact,changes +func (g *Groupware) GetContactsChanges(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + ok, accountId, resp := req.needContactWithAccount() + if !ok { + return resp + } + + l := req.logger.With() + + var maxChanges uint = 0 + if v, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0); err != nil { + return req.error(accountId, err) + } else if ok { + maxChanges = v + l = l.Uint(QueryParamMaxChanges, v) + } + + sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes") + if err != nil { + return req.error(accountId, err) + } + l = l.Str(HeaderParamSince, log.SafeString(sinceState)) + + logger := log.From(l) + changes, sessionState, state, lang, jerr := g.jmap.GetContactCardsSince(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges) + if jerr != nil { + return req.jmapError(accountId, jerr, sessionState, lang) + } + var body jmap.ContactCardChanges = changes + + return req.respond(accountId, body, sessionState, ContactResponseObjectType, state) + }) +} + func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { ok, accountId, resp := req.needContactWithAccount() @@ -186,22 +242,22 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) { addressBookId, err := req.PathParam(UriParamAddressBookId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) var create jscontact.ContactCard err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set") if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(l) created, sessionState, state, lang, jerr := g.jmap.CreateContactCard(accountId, req.session, req.ctx, logger, req.language(), create) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, ContactResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, ContactResponseObjectType, state) }) } @@ -215,33 +271,33 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) { contactId, err := req.PathParam(UriParamContactId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamContactId, log.SafeString(contactId)) logger := log.From(l) - deleted, sessionState, state, _, jerr := g.jmap.DeleteContactCard(accountId, []string{contactId}, req.session, req.ctx, logger, req.language()) + deleted, sessionState, state, lang, jerr := g.jmap.DeleteContactCard(accountId, []string{contactId}, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } for _, e := range deleted { desc := e.Description if desc != "" { - return errorResponseWithSessionState(single(accountId), apiError( + return req.error(accountId, apiError( req.errorId(), ErrorFailedToDeleteContact, withDetail(e.Description), - ), sessionState) + )) } else { - return errorResponseWithSessionState(single(accountId), apiError( + return req.error(accountId, apiError( req.errorId(), ErrorFailedToDeleteContact, - ), sessionState) + )) } } - return noContentResponseWithEtag(single(accountId), sessionState, ContactResponseObjectType, state) + return req.noContent(accountId, sessionState, ContactResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index 2c754f5e64..57a44ce40b 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -21,44 +21,41 @@ import ( "github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics" ) -// Get all the emails in a mailbox since a given state. -// -// Retrieve the list of all the emails that are in a given mailbox since a given state. -// -// The mailbox must be specified by its id, as part of the request URL path. -// -// A limit and an offset may be specified using the query parameters 'limit' and 'offset', -// respectively. -func (g *Groupware) GetAllEmailsInMailboxSince(w http.ResponseWriter, r *http.Request) { - - maxChanges := uint(0) +// Get the changes that occured in a given mailbox since a certain state. +// @api:tags mailbox,changes +func (g *Groupware) GetEmailChanges(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { + l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } + l = l.Str(logAccountId, accountId) - mailboxId, err := req.PathParam(UriParamMailboxId) + maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) + } + if ok { + l = l.Uint(QueryParamMaxChanges, maxChanges) } - since, err := req.PathParamDoc(UriParamSince, "State identifier that indicates the coordinate from whence on to list mailbox changes") - if err != nil { - return errorResponse(single(accountId), err) + sinceState := jmap.EmptyState + if s := req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list email changes"); s != "" { + l = l.Str(HeaderParamSince, log.SafeString(s)) + sinceState = jmap.State(s) } - logger := log.From(req.logger.With().Str(HeaderParamSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId))) + logger := log.From(l) - changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges) + changes, sessionState, state, lang, jerr := g.jmap.GetEmailChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, true, g.config.maxBodyValueBytes, maxChanges) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state) }) - } // Get all the emails in a mailbox. @@ -75,18 +72,18 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, accountId) mailboxId, err := req.PathParam(UriParamMailboxId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } offset, ok, err := req.parseIntParam(QueryParamOffset, 0) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Int(QueryParamOffset, offset) @@ -94,7 +91,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint(QueryParamLimit, limit) @@ -108,12 +105,12 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } sanitized, err := req.sanitizeEmails(emails.Emails) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } safe := jmap.Emails{ @@ -123,7 +120,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request Offset: emails.Offset, } - return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, safe, sessionState, EmailResponseObjectType, state) }) } @@ -175,13 +172,13 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } ids := strings.Split(id, ",") if len(ids) < 1 { @@ -190,7 +187,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { markAsSeen, ok, err := req.parseBoolParam(QueryParamMarkAsSeen, false) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Bool(QueryParamMarkAsSeen, markAsSeen) @@ -201,32 +198,32 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.config.maxBodyValueBytes, markAsSeen, true) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(emails) < 1 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EmailResponseObjectType, state) } else { sanitized, err := req.sanitizeEmail(emails[0]) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } - return etagResponse(single(accountId), sanitized, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state) } } else { logger := log.From(l.Array("ids", log.SafeStringArray(ids))) emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.config.maxBodyValueBytes, markAsSeen, false) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(emails) < 1 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EmailResponseObjectType, state) } else { sanitized, err := req.sanitizeEmails(emails) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } - return etagResponse(single(accountId), sanitized, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state) } } }) @@ -259,29 +256,29 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(l) emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(emails) < 1 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EmailResponseObjectType, state) } email, err := req.sanitizeEmail(emails[0]) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } var body []jmap.EmailBodyPart = email.Attachments - return etagResponse(single(accountId), body, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, body, sessionState, EmailResponseObjectType, state) }) } else { g.stream(w, r, func(req Request, w http.ResponseWriter) *Error { @@ -377,13 +374,13 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) @@ -391,12 +388,12 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since logger := log.From(l) - changes, sessionState, state, lang, jerr := g.jmap.GetEmailsSince(accountId, req.session, req.ctx, logger, req.language(), since, true, g.config.maxBodyValueBytes, maxChanges) + changes, sessionState, state, lang, jerr := g.jmap.GetEmailChanges(accountId, req.session, req.ctx, logger, req.language(), since, true, g.config.maxBodyValueBytes, maxChanges) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, changes, sessionState, EmailResponseObjectType, state) }) } @@ -604,12 +601,12 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req) if !ok { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger = log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId))) @@ -621,7 +618,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.config.maxBodyValueBytes) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if results, ok := resultsByAccount[accountId]; ok { @@ -641,7 +638,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { } sanitized, err := req.sanitizeEmail(result.Email) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } flattened[i] = EmailWithSnippets{ Email: sanitized, @@ -649,14 +646,14 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { } } - return etagResponse(single(accountId), EmailWithSnippetsSearchResults{ + return req.respond(accountId, EmailWithSnippetsSearchResults{ Results: flattened, Total: results.Total, Limit: results.Limit, QueryState: results.QueryState, - }, sessionState, EmailResponseObjectType, state, lang) + }, sessionState, EmailResponseObjectType, state) } else { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EmailResponseObjectType, state) } }) } @@ -668,7 +665,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req) if !ok { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } logger = log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(allAccountIds))) @@ -679,7 +676,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque if makesSnippets { resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit) if jerr != nil { - return req.errorResponseFromJmap(allAccountIds, jerr) + return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } var totalOverAllAccounts uint = 0 @@ -713,13 +710,13 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque QueryState: state, } - return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang) + return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state) } else { withThreads := true resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit, withThreads) if jerr != nil { - return req.errorResponseFromJmap(allAccountIds, jerr) + return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } var totalAcrossAllAccounts uint = 0 @@ -752,7 +749,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque QueryState: state, } - return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang) + return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state) } }) } @@ -763,9 +760,9 @@ var draftEmailAutoMailboxRolePrecedence = []string{ } func findDraftsMailboxId(j *jmap.Client, accountId string, req Request, logger *log.Logger) (string, Response) { - mailboxIdsPerAccountIds, _, _, _, jerr := j.SearchMailboxIdsPerRole(single(accountId), req.session, req.ctx, logger, req.language(), draftEmailAutoMailboxRolePrecedence) + mailboxIdsPerAccountIds, sessionState, _, lang, jerr := j.SearchMailboxIdsPerRole(single(accountId), req.session, req.ctx, logger, req.language(), draftEmailAutoMailboxRolePrecedence) if jerr != nil { - return "", req.errorResponseFromJmap(single(accountId), jerr) + return "", req.jmapError(accountId, jerr, sessionState, lang) } else { for _, role := range draftEmailAutoMailboxRolePrecedence { if mailboxId, ok := mailboxIdsPerAccountIds[accountId][role]; ok { @@ -774,7 +771,7 @@ func findDraftsMailboxId(j *jmap.Client, accountId string, req Request, logger * } // couldn't find a Mailbox with the drafts role for that account, // we have to return an error... ? - return "", errorResponse(single(accountId), apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) + return "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) } } @@ -786,9 +783,9 @@ var sentEmailAutoMailboxRolePrecedence = []string{ var draftAndSentMailboxRoles = structs.Uniq(structs.Concat(draftEmailAutoMailboxRolePrecedence, sentEmailAutoMailboxRolePrecedence)) func findSentMailboxId(j *jmap.Client, accountId string, req Request, logger *log.Logger) (string, string, Response) { - mailboxIdsPerAccountIds, _, _, _, jerr := j.SearchMailboxIdsPerRole(single(accountId), req.session, req.ctx, logger, req.language(), draftAndSentMailboxRoles) + mailboxIdsPerAccountIds, sessionState, _, lang, jerr := j.SearchMailboxIdsPerRole(single(accountId), req.session, req.ctx, logger, req.language(), draftAndSentMailboxRoles) if jerr != nil { - return "", "", req.errorResponseFromJmap(single(accountId), jerr) + return "", "", req.jmapError(accountId, jerr, sessionState, lang) } else { sentMailboxId := "" for _, role := range sentEmailAutoMailboxRolePrecedence { @@ -798,7 +795,7 @@ func findSentMailboxId(j *jmap.Client, accountId string, req Request, logger *lo } } if sentMailboxId == "" { - return "", "", errorResponse(single(accountId), apiError(req.errorId(), ErrorNoMailboxWithSentRole)) + return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithSentRole)) } draftsMailboxId := "" for _, role := range draftEmailAutoMailboxRolePrecedence { @@ -808,7 +805,7 @@ func findSentMailboxId(j *jmap.Client, accountId string, req Request, logger *lo } } if draftsMailboxId == "" { - return "", "", errorResponse(single(accountId), apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) + return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) } return draftsMailboxId, sentMailboxId, Response{} } @@ -820,14 +817,14 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId))) var body jmap.EmailCreate err := req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if len(body.MailboxIds) < 1 { @@ -841,10 +838,10 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) { created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, "", req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, EmailResponseObjectType, state) }) } @@ -854,12 +851,12 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } replaceId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId))) @@ -867,7 +864,7 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) { var body jmap.EmailCreate err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if len(body.MailboxIds) < 1 { @@ -881,10 +878,10 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) { created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, replaceId, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, EmailResponseObjectType, state) }) } @@ -894,13 +891,13 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, accountId) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -909,7 +906,7 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { var body map[string]any err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } updates := map[string]jmap.EmailUpdate{ @@ -918,20 +915,20 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if result == nil { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) } updatedEmail, ok := result[emailId] if !ok { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } - return etagResponse(single(accountId), updatedEmail, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state) }) } @@ -950,13 +947,13 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, accountId) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -965,11 +962,11 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) var body emailKeywordUpdates err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if body.IsEmpty() { - return noContentResponse(single(accountId), req.session.State) + return req.noop(accountId) } patch := jmap.EmailUpdate{} @@ -985,20 +982,20 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if result == nil { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) } updatedEmail, ok := result[emailId] if !ok { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } - return etagResponse(single(accountId), updatedEmail, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state) }) } @@ -1009,13 +1006,13 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, accountId) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -1024,11 +1021,11 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { var body []string err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if len(body) < 1 { - return noContentResponse(single(accountId), req.session.State) + return req.noop(accountId) } patch := jmap.EmailUpdate{} @@ -1041,23 +1038,23 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if result == nil { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) } updatedEmail, ok := result[emailId] if !ok { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } if updatedEmail == nil { - return noContentResponseWithEtag(single(accountId), sessionState, EmailResponseObjectType, state) + return req.noContent(accountId, sessionState, EmailResponseObjectType, state) } else { - return etagResponse(single(accountId), updatedEmail, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state) } }) } @@ -1069,13 +1066,13 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(logAccountId, accountId) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -1084,11 +1081,11 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) var body []string err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if len(body) < 1 { - return noContentResponse(single(accountId), req.session.State) + return req.noop(accountId) } patch := jmap.EmailUpdate{} @@ -1101,23 +1098,23 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if result == nil { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) } updatedEmail, ok := result[emailId] if !ok { - return errorResponse(single(accountId), apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } if updatedEmail == nil { - return noContentResponseWithEtag(single(accountId), sessionState, EmailResponseObjectType, state) + return req.noContent(accountId, sessionState, EmailResponseObjectType, state) } else { - return etagResponse(single(accountId), updatedEmail, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state) } }) } @@ -1129,39 +1126,39 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, log.SafeString(accountId)) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) logger := log.From(l) - resp, sessionState, state, _, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger, req.language()) + resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } for _, e := range resp { desc := e.Description if desc != "" { - return errorResponseWithSessionState(single(accountId), apiError( + return req.error(accountId, apiError( req.errorId(), ErrorFailedToDeleteEmail, withDetail(e.Description), - ), sessionState) + )) } else { - return errorResponseWithSessionState(single(accountId), apiError( + return req.error(accountId, apiError( req.errorId(), ErrorFailedToDeleteEmail, - ), sessionState) + )) } } - return noContentResponseWithEtag(single(accountId), sessionState, EmailResponseObjectType, state) + return req.noContent(accountId, sessionState, EmailResponseObjectType, state) }) } @@ -1176,23 +1173,23 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, accountId) var emailIds []string err := req.body(&emailIds) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Array("emailIds", log.SafeStringArray(emailIds)) logger := log.From(l) - resp, sessionState, state, _, jerr := g.jmap.DeleteEmails(accountId, emailIds, req.session, req.ctx, logger, req.language()) + resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, emailIds, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(resp) > 0 { @@ -1200,13 +1197,13 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) { for emailId, e := range resp { meta[emailId] = e.Description } - return errorResponseWithSessionState(single(accountId), apiError( + return req.error(accountId, apiError( req.errorId(), ErrorFailedToDeleteEmail, withMeta(meta), - ), sessionState) + )) } - return noContentResponseWithEtag(single(accountId), sessionState, EmailResponseObjectType, state) + return req.noContent(accountId, sessionState, EmailResponseObjectType, state) }) } @@ -1216,19 +1213,19 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return errorResponse(single(accountId), gwerr) + return req.error(accountId, gwerr) } l.Str(logAccountId, accountId) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) identityId, err := req.getMandatoryStringParam(QueryParamIdentityId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l.Str(QueryParamIdentityId, log.SafeString(identityId)) @@ -1261,10 +1258,10 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { resp, sessionState, state, lang, jerr := g.jmap.SubmitEmail(accountId, identityId, emailId, move, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), resp, sessionState, EmailResponseObjectType, state, lang) + return req.respond(accountId, resp, sessionState, EmailResponseObjectType, state) }) } @@ -1330,21 +1327,21 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { l := req.logger.With() - accountId, gwerr := req.GetAccountIdForMail() - if gwerr != nil { - return errorResponse(single(accountId), gwerr) + accountId, err := req.GetAccountIdForMail() + if err != nil { + return req.error(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParam(UriParamEmailId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logEmailId, log.SafeString(id)) limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint("limit", limit) @@ -1352,7 +1349,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { days, ok, err := req.parseUIntParam(QueryParamDays, 5) // TODO configurable default days if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint("days", days) @@ -1365,13 +1362,13 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.config.maxBodyValueBytes, false, false) getEmailsDuration := time.Since(getEmailsBefore) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(emails) < 1 { req.observe(g.metrics.EmailByIdDuration.WithLabelValues(req.session.JmapEndpoint, metrics.Values.Result.NotFound), getEmailsDuration.Seconds()) logger.Trace().Msg("failed to find any emails matching id") // the id is already in the log field - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, EmailResponseObjectType, state) } else { req.observe(g.metrics.EmailByIdDuration.WithLabelValues(req.session.JmapEndpoint, metrics.Values.Result.Found), getEmailsDuration.Seconds()) } @@ -1408,7 +1405,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { g.job(logger, RelationTypeSameThread, func(jobId uint64, l *log.Logger) { before := time.Now() - emails, _, _, _, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, req.language(), false, g.config.maxBodyValueBytes) + emails, _, _, lang, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, req.language(), false, g.config.maxBodyValueBytes) duration := time.Since(before) if jerr != nil { _ = req.observeJmapError(jerr) @@ -1427,12 +1424,12 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { sanitized, err := req.sanitizeEmail(email) if err != nil { - return errorResponseWithSessionState(single(accountId), err, sessionState) + return req.error(accountId, err) } - return etagResponse(single(accountId), AboutEmailResponse{ + return req.respond(accountId, AboutEmailResponse{ Email: sanitized, RequestId: reqId, - }, sessionState, EmailResponseObjectType, state, lang) + }, sessionState, EmailResponseObjectType, state) }) } @@ -1630,7 +1627,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO from configuration if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if ok { l = l.Uint(QueryParamLimit, limit) @@ -1638,10 +1635,10 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, offset, ok, err := req.parseUIntParam(QueryParamOffset, 0) if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if offset > 0 { - return notImplementedResponse() + return req.notImplementedN(allAccountIds, EmailResponseObjectType) } if ok { l = l.Uint(QueryParamOffset, limit) @@ -1649,7 +1646,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, seen, ok, err := req.parseBoolParam(QueryParamSeen, false) if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if ok { l = l.Bool(QueryParamSeen, seen) @@ -1657,7 +1654,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, undesirable, ok, err := req.parseBoolParam(QueryParamUndesirable, false) if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if ok { l = l.Bool(QueryParamUndesirable, undesirable) @@ -1679,7 +1676,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, emailsSummariesByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit, true) if jerr != nil { - return req.errorResponseFromJmap(allAccountIds, jerr) + return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } // sort in memory to respect the overall limit @@ -1703,12 +1700,12 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, summaries[i] = summarizeEmail(all[i].accountId, all[i].email) } - return etagResponse(allAccountIds, EmailSummaries{ + return req.respondN(allAccountIds, EmailSummaries{ Emails: summaries, Total: total, Limit: limit, Offset: offset, - }, sessionState, EmailResponseObjectType, state, lang) + }, sessionState, EmailResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_identity.go b/services/groupware/pkg/groupware/api_identity.go index 9ff339a6b4..efca6bbaa3 100644 --- a/services/groupware/pkg/groupware/api_identity.go +++ b/services/groupware/pkg/groupware/api_identity.go @@ -15,14 +15,14 @@ func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) res, sessionState, state, lang, jerr := g.jmap.GetAllIdentities(accountId, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), res, sessionState, IdentityResponseObjectType, state, lang) + return req.respond(accountId, res, sessionState, IdentityResponseObjectType, state) }) } @@ -30,22 +30,22 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } id, err := req.PathParam(UriParamIdentityId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id)) res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id}) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(res) < 1 { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, IdentityResponseObjectType, state) } var body jmap.Identity = res[0] - return etagResponse(single(accountId), body, sessionState, IdentityResponseObjectType, state, lang) + return req.respond(accountId, body, sessionState, IdentityResponseObjectType, state) }) } @@ -53,21 +53,21 @@ func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) var identity jmap.Identity err = req.body(&identity) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } created, sessionState, state, lang, jerr := g.jmap.CreateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, IdentityResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, IdentityResponseObjectType, state) }) } @@ -75,21 +75,21 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) var identity jmap.Identity err = req.body(&identity) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } updated, sessionState, state, lang, jerr := g.jmap.UpdateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), updated, sessionState, IdentityResponseObjectType, state, lang) + return req.respond(accountId, updated, sessionState, IdentityResponseObjectType, state) }) } @@ -98,30 +98,30 @@ func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) id, err := req.PathParam(UriParamIdentityId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } ids := strings.Split(id, ",") if len(ids) < 1 { return req.parameterErrorResponse(single(accountId), UriParamIdentityId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids")) } - deletion, sessionState, state, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids) + deletion, sessionState, state, lang, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } notDeletedIds := structs.Missing(ids, deletion) if len(notDeletedIds) == 0 { - return noContentResponseWithEtag(single(accountId), sessionState, IdentityResponseObjectType, state) + return req.noContent(accountId, sessionState, IdentityResponseObjectType, state) } else { logger.Error().Array("not-deleted", log.SafeStringArray(notDeletedIds)).Msgf("failed to delete %d identities", len(notDeletedIds)) - return errorResponseWithSessionState(single(accountId), req.apiError(&ErrorFailedToDeleteSomeIdentities, + return req.errorS(accountId, req.apiError(&ErrorFailedToDeleteSomeIdentities, withMeta(map[string]any{"ids": notDeletedIds})), sessionState) } }) diff --git a/services/groupware/pkg/groupware/api_index.go b/services/groupware/pkg/groupware/api_index.go index f6991e4835..015152054d 100644 --- a/services/groupware/pkg/groupware/api_index.go +++ b/services/groupware/pkg/groupware/api_index.go @@ -150,7 +150,7 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) { boot, sessionState, state, lang, err := g.jmap.GetBootstrap(accountIds, req.session, req.ctx, req.logger, req.language()) if err != nil { - return req.errorResponseFromJmap(accountIds, err) + return req.jmapErrorN(accountIds, err, sessionState, lang) } var body IndexResponse = IndexResponse{ @@ -160,7 +160,7 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) { Accounts: buildIndexAccounts(req.session, boot), PrimaryAccounts: buildIndexPrimaryAccounts(req.session), } - return etagResponse(accountIds, body, sessionState, IndexResponseObjectType, state, lang) + return req.respondN(accountIds, body, sessionState, IndexResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go index acbac71a67..c571d9e855 100644 --- a/services/groupware/pkg/groupware/api_mailbox.go +++ b/services/groupware/pkg/groupware/api_mailbox.go @@ -21,23 +21,23 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } mailboxId, err := req.PathParam(UriParamMailboxId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId}) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } if len(mailboxes.Mailboxes) == 1 { - return etagResponse(single(accountId), mailboxes.Mailboxes[0], sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, mailboxes.Mailboxes[0], sessionState, MailboxResponseObjectType, state) } else { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, MailboxResponseObjectType, state) } }) } @@ -69,12 +69,12 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if set { filter.IsSubscribed = &subscribed @@ -86,23 +86,23 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { if hasCriteria { mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(single(accountId), req.session, req.ctx, logger, req.language(), filter) if err != nil { - return req.errorResponseFromJmap(single(accountId), err) + return req.jmapError(accountId, err, sessionState, lang) } if mailboxes, ok := mailboxesByAccountId[accountId]; ok { - return etagResponse(single(accountId), sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state) } else { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, MailboxResponseObjectType, state) } } else { mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes(single(accountId), req.session, req.ctx, logger, req.language()) if err != nil { - return req.errorResponseFromJmap(single(accountId), err) + return req.jmapError(accountId, err, sessionState, lang) } if mailboxes, ok := mailboxesByAccountId[accountId]; ok { - return etagResponse(single(accountId), sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state) } else { - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, MailboxResponseObjectType, state) } } }) @@ -113,7 +113,7 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return noContentResponse(nil, "") // when the user has no accounts + return req.noopN(nil) // when the user has no accounts } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds))) @@ -126,7 +126,7 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re } if subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false); err != nil { - return errorResponse(accountIds, err) + return req.errorN(accountIds, err) } else if set { filter.IsSubscribed = &subscribed hasCriteria = true @@ -135,15 +135,15 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re if hasCriteria { mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter) if err != nil { - return req.errorResponseFromJmap(accountIds, err) + return req.jmapErrorN(accountIds, err, sessionState, lang) } - return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang) + return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state) } else { mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger, req.language()) if err != nil { - return req.errorResponseFromJmap(accountIds, err) + return req.jmapErrorN(accountIds, err, sessionState, lang) } - return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang) + return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state) } }) } @@ -153,12 +153,12 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return noContentResponse(nil, "") // when the user has no accounts + return req.noopN(accountIds) // when the user has no accounts } role, err := req.PathParamDoc(UriParamRole, "Role of the mailboxes to retrieve across all accounts") if err != nil { - return errorResponse(accountIds, err) + return req.errorN(accountIds, err) } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)).Str("role", role)) @@ -169,50 +169,58 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt mailboxesByAccountId, sessionState, state, lang, jerr := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter) if jerr != nil { - return req.errorResponseFromJmap(accountIds, jerr) + return req.jmapErrorN(accountIds, jerr, sessionState, lang) } - return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang) + return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state) }) } // Get the changes that occured in a given mailbox since a certain state. +// @api:tags mailbox,changes func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, accountId) - mailboxId, err := req.PathParam(UriParamMailboxId) - if err != nil { - return errorResponse(single(accountId), err) - } - maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) } - sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes") - if err != nil { - return errorResponse(single(accountId), err) + sinceState := req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list mailbox changes") + if sinceState != "" { + l = l.Str(HeaderParamSince, log.SafeString(sinceState)) } - l = l.Str(HeaderParamSince, log.SafeString(sinceState)) logger := log.From(l) - changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.config.maxBodyValueBytes, maxChanges) - if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + // As for Emails and Contacts, one would expect this request without any prior state to + // be usable to list all the objects that currently exist, but that is not the case for + // Mailbox, at least in combination with Stalwart, as those are initial objects that + // "always existed", even with the initial State, and this the Mailbox/changes operation + // returns nothing. + // For this reason, when the "since" state is empty, we respond with an error. + if sinceState == "" { + return req.error(accountId, req.apiError(&ErrorInvalidUserRequest, withTitle( + "Mailbox changes without state is unsupported", + "Requesting Mailbox changes without an initial state is an unsupported operation", + ))) } - return etagResponse(single(accountId), changes, sessionState, MailboxResponseObjectType, state, lang) + changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges) + if jerr != nil { + return req.jmapError(accountId, jerr, sessionState, lang) + } + + return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state) }) } @@ -226,7 +234,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht sinceStateMap, ok, err := req.parseMapParam(QueryParamSince) if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if ok { dict := zerolog.Dict() @@ -238,7 +246,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return errorResponse(allAccountIds, err) + return req.errorN(allAccountIds, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) @@ -246,12 +254,12 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht logger := log.From(l) - changesByAccountId, sessionState, state, lang, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language(), sinceStateMap, true, g.config.maxBodyValueBytes, maxChanges) + changesByAccountId, sessionState, state, lang, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language(), sinceStateMap, maxChanges) if jerr != nil { - return req.errorResponseFromJmap(allAccountIds, jerr) + return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } - return etagResponse(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state, lang) + return req.respondN(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state) }) } @@ -266,10 +274,10 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) { rolesByAccountId, sessionState, state, lang, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(allAccountIds, jerr) + return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } - return etagResponse(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state, lang) + return req.respondN(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state) }) } @@ -279,29 +287,29 @@ func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) { accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, accountId) mailboxId, err := req.PathParamDoc(UriParamMailboxId, "the identifier of the mailbox to update") if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(UriParamMailboxId, log.SafeString(mailboxId)) var body jmap.MailboxChange err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(l) updated, sessionState, state, lang, jerr := g.jmap.UpdateMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, "", body) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), updated, sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, updated, sessionState, MailboxResponseObjectType, state) }) } @@ -310,23 +318,23 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) { l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, accountId) var body jmap.MailboxChange err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(l) created, sessionState, state, lang, jerr := g.jmap.CreateMailbox(accountId, req.session, req.ctx, logger, req.language(), "", body) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), created, sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, created, sessionState, MailboxResponseObjectType, state) }) } @@ -340,28 +348,28 @@ func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) { l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Str(logAccountId, accountId) mailboxIds, err := req.PathListParamDoc(UriParamMailboxId, "the identifier of the mailbox to delete") if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds)) if len(mailboxIds) < 1 { - return noContentResponse(single(accountId), req.session.State) // no mailbox identifiers were mentioned in the request + return req.noop(accountId) // no mailbox identifiers were mentioned in the request } logger := log.From(l) deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, req.session, req.ctx, logger, req.language(), "", mailboxIds) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), deleted, sessionState, MailboxResponseObjectType, state, lang) + return req.respond(accountId, deleted, sessionState, MailboxResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_quota.go b/services/groupware/pkg/groupware/api_quota.go index 437791668f..bfdbdeeabd 100644 --- a/services/groupware/pkg/groupware/api_quota.go +++ b/services/groupware/pkg/groupware/api_quota.go @@ -16,19 +16,19 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForQuota() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) res, sessionState, state, lang, jerr := g.jmap.GetQuotas(single(accountId), req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } for _, v := range res { body := v.List - return etagResponse(single(accountId), body, sessionState, QuotaResponseObjectType, state, lang) + return req.respond(accountId, body, sessionState, QuotaResponseObjectType, state) } - return notFoundResponse(single(accountId), sessionState) + return req.notFound(accountId, sessionState, QuotaResponseObjectType, state) }) } @@ -45,13 +45,13 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return noContentResponse(accountIds, "") // user has no accounts + return req.noopN(accountIds) // user has no accounts } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds))) res, sessionState, state, lang, jerr := g.jmap.GetQuotas(accountIds, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(accountIds, jerr) + return req.jmapErrorN(accountIds, jerr, sessionState, lang) } result := make(map[string]AccountQuota, len(res)) @@ -61,6 +61,6 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques Quotas: accountQuotas.List, } } - return etagResponse(accountIds, result, sessionState, QuotaResponseObjectType, state, lang) + return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/api_tasklists.go b/services/groupware/pkg/groupware/api_tasklists.go index eb5cbe1a1c..b2d110c681 100644 --- a/services/groupware/pkg/groupware/api_tasklists.go +++ b/services/groupware/pkg/groupware/api_tasklists.go @@ -16,7 +16,7 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) { var _ string = accountId var body []jmap.TaskList = AllTaskLists - return etagResponse(single(accountId), body, req.session.State, TaskListResponseObjectType, TaskListsState, "") + return req.respond(accountId, body, req.session.State, TaskListResponseObjectType, TaskListsState) }) } @@ -31,15 +31,15 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) { tasklistId, err := req.PathParam(UriParamTaskListId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } // TODO replace with proper implementation for _, tasklist := range AllTaskLists { if tasklist.Id == tasklistId { - return response(single(accountId), tasklist, req.session.State, "") + return req.respond(accountId, tasklist, req.session.State, TaskListResponseObjectType, TaskListsState) } } - return etagNotFoundResponse(single(accountId), req.session.State, TaskListResponseObjectType, TaskListsState, "") + return req.etaggedNotFound(accountId, req.session.State, TaskListResponseObjectType, TaskListsState) }) } @@ -54,13 +54,13 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) { tasklistId, err := req.PathParam(UriParamTaskListId) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } // TODO replace with proper implementation tasks, ok := TaskMapByTaskListId[tasklistId] if !ok { - return notFoundResponse(single(accountId), req.session.State) + return req.notFound(accountId, req.session.State, TaskResponseObjectType, TaskState) } - return etagResponse(single(accountId), tasks, req.session.State, TaskResponseObjectType, TaskState, "") + return req.respond(accountId, tasks, req.session.State, TaskResponseObjectType, TaskState) }) } diff --git a/services/groupware/pkg/groupware/api_vacation.go b/services/groupware/pkg/groupware/api_vacation.go index 5885b1c92c..10627e2f9c 100644 --- a/services/groupware/pkg/groupware/api_vacation.go +++ b/services/groupware/pkg/groupware/api_vacation.go @@ -17,15 +17,15 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForVacationResponse() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) res, sessionState, state, lang, jerr := g.jmap.GetVacationResponse(accountId, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), res, sessionState, VacationResponseResponseObjectType, state, lang) + return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state) }) } @@ -37,21 +37,21 @@ func (g *Groupware) SetVacation(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForVacationResponse() if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) var body jmap.VacationResponsePayload err = req.body(&body) if err != nil { - return errorResponse(single(accountId), err) + return req.error(accountId, err) } res, sessionState, state, lang, jerr := g.jmap.SetVacationResponse(accountId, body, req.session, req.ctx, logger, req.language()) if jerr != nil { - return req.errorResponseFromJmap(single(accountId), jerr) + return req.jmapError(accountId, jerr, sessionState, lang) } - return etagResponse(single(accountId), res, sessionState, VacationResponseResponseObjectType, state, lang) + return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state) }) } diff --git a/services/groupware/pkg/groupware/error.go b/services/groupware/pkg/groupware/error.go index b1a035c522..5bb513476a 100644 --- a/services/groupware/pkg/groupware/error.go +++ b/services/groupware/pkg/groupware/error.go @@ -651,6 +651,22 @@ func errorResponses(errors ...Error) ErrorResponse { return ErrorResponse{Errors: errors} } -func (r *Request) errorResponseFromJmap(accountIds []string, err jmap.Error) Response { - return errorResponseWithSessionState(accountIds, r.apiErrorFromJmap(r.observeJmapError(err)), r.session.State) +func (r *Request) error(accountId string, err *Error) Response { + return errorResponse(single(accountId), err, r.session.State, jmap.NoLanguage) +} + +func (r *Request) errorS(accountId string, err *Error, sessionState jmap.SessionState) Response { + return errorResponse(single(accountId), err, sessionState, jmap.NoLanguage) +} + +func (r *Request) errorN(accountIds []string, err *Error) Response { + return errorResponse(accountIds, err, r.session.State, jmap.NoLanguage) +} + +func (r *Request) jmapError(accountId string, err jmap.Error, sessionState jmap.SessionState, lang jmap.Language) Response { + return errorResponse(single(accountId), r.apiErrorFromJmap(r.observeJmapError(err)), sessionState, lang) +} + +func (r *Request) jmapErrorN(accountIds []string, err jmap.Error, sessionState jmap.SessionState, lang jmap.Language) Response { + return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(err)), sessionState, lang) } diff --git a/services/groupware/pkg/groupware/request.go b/services/groupware/pkg/groupware/request.go index 26ce6f0f19..385280f2bf 100644 --- a/services/groupware/pkg/groupware/request.go +++ b/services/groupware/pkg/groupware/request.go @@ -223,7 +223,7 @@ func (r *Request) parameterError(param string, detail string) *Error { } func (r *Request) parameterErrorResponse(accountIds []string, param string, detail string) Response { - return errorResponse(accountIds, r.parameterError(param, detail)) + return r.errorN(accountIds, r.parameterError(param, detail)) } func (r *Request) getStringParam(param string, defaultValue string) (string, bool) { @@ -427,7 +427,7 @@ func (r *Request) observeJmapError(jerr jmap.Error) jmap.Error { func (r *Request) needTask(accountId string) (bool, Response) { if !IgnoreSessionCapabilityChecksForTasks { if r.session.Capabilities.Tasks == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State, jmap.Language(r.language())) } } return true, Response{} @@ -439,10 +439,10 @@ func (r *Request) needTaskForAccount(accountId string) (bool, Response) { } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) } if account.AccountCapabilities.Tasks == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State, jmap.NoLanguage) } return true, Response{} } @@ -450,7 +450,7 @@ func (r *Request) needTaskForAccount(accountId string) (bool, Response) { func (r *Request) needTaskWithAccount() (bool, string, Response) { accountId, err := r.GetAccountIdForTask() if err != nil { - return false, "", errorResponse(single(accountId), err) + return false, "", r.error(accountId, err) } if ok, resp := r.needTaskForAccount(accountId); !ok { return false, accountId, resp @@ -460,7 +460,7 @@ func (r *Request) needTaskWithAccount() (bool, string, Response) { func (r *Request) needCalendar(accountId string) (bool, Response) { if r.session.Capabilities.Calendars == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State, jmap.NoLanguage) } return true, Response{} } @@ -471,10 +471,10 @@ func (r *Request) needCalendarForAccount(accountId string) (bool, Response) { } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) } if account.AccountCapabilities.Calendars == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State, jmap.NoLanguage) } return true, Response{} } @@ -482,7 +482,7 @@ func (r *Request) needCalendarForAccount(accountId string) (bool, Response) { func (r *Request) needCalendarWithAccount() (bool, string, Response) { accountId, err := r.GetAccountIdForCalendar() if err != nil { - return false, "", errorResponse(single(accountId), err) + return false, "", r.error(accountId, err) } if ok, resp := r.needCalendarForAccount(accountId); !ok { return false, accountId, resp @@ -492,7 +492,7 @@ func (r *Request) needCalendarWithAccount() (bool, string, Response) { func (r *Request) needContact(accountId string) (bool, Response) { if r.session.Capabilities.Contacts == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State, jmap.NoLanguage) } return true, Response{} } @@ -503,10 +503,10 @@ func (r *Request) needContactForAccount(accountId string) (bool, Response) { } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) } if account.AccountCapabilities.Contacts == nil { - return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingContactsAccountCapability), r.session.State) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsAccountCapability), r.session.State, jmap.NoLanguage) } return true, Response{} } @@ -514,7 +514,7 @@ func (r *Request) needContactForAccount(accountId string) (bool, Response) { func (r *Request) needContactWithAccount() (bool, string, Response) { accountId, err := r.GetAccountIdForContact() if err != nil { - return false, "", errorResponse(single(accountId), err) + return false, "", r.error(accountId, err) } if ok, resp := r.needContactForAccount(accountId); !ok { return false, accountId, resp @@ -565,7 +565,7 @@ func (r *Request) parseSort(s string, props []string) ([]SortCrit, *Error) { func mapSort[T any](accountIds []string, req *Request, defaultSort []T, props []string, mapper func(SortCrit) T) ([]T, bool, Response) { if sortSpec, ok := req.getStringParam(QueryParamSort, ""); ok && strings.TrimSpace(sortSpec) != "" { if sort, err := req.parseSort(sortSpec, props); err != nil { - return nil, false, errorResponseWithSessionState(accountIds, err, req.session.State) + return nil, false, errorResponse(accountIds, err, req.session.State, jmap.NoLanguage) } else { return structs.Map(sort, mapper), true, Response{} } diff --git a/services/groupware/pkg/groupware/response.go b/services/groupware/pkg/groupware/response.go index 74366bf89b..4fd3369903 100644 --- a/services/groupware/pkg/groupware/response.go +++ b/services/groupware/pkg/groupware/response.go @@ -36,23 +36,14 @@ type Response struct { contentLanguage jmap.Language } -func errorResponse(accountIds []string, err *Error) Response { +func errorResponse(accountIds []string, err *Error, sessionState jmap.SessionState, contentLanguage jmap.Language) Response { return Response{ - accountIds: accountIds, - body: nil, - err: err, - etag: "", - sessionState: "", - } -} - -func errorResponseWithSessionState(accountIds []string, err *Error, sessionState jmap.SessionState) Response { - return Response{ - accountIds: accountIds, - body: nil, - err: err, - etag: "", - sessionState: sessionState, + accountIds: accountIds, + body: nil, + err: err, + etag: "", + sessionState: sessionState, + contentLanguage: contentLanguage, } } @@ -67,7 +58,11 @@ func response(accountIds []string, body any, sessionState jmap.SessionState, con } } -func etagResponse(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { +func (r *Request) respondWithoutStatus(accountId string, body any) Response { + return response(single(accountId), body, r.session.State, jmap.Language(r.language())) +} + +func etaggedResponse(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { return Response{ accountIds: accountIds, body: body, @@ -79,6 +74,14 @@ func etagResponse(accountIds []string, body any, sessionState jmap.SessionState, } } +func (r *Request) respond(accountId string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return etaggedResponse(single(accountId), body, sessionState, objectType, etag, jmap.Language(r.language())) +} + +func (r *Request) respondN(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return etaggedResponse(accountIds, body, sessionState, objectType, etag, jmap.Language(r.language())) +} + /* func etagOnlyResponse(body any, etag jmap.State, objectType ResponseObjectType, contentLanguage jmap.Language) Response { return Response{ @@ -103,6 +106,14 @@ func noContentResponse(accountIds []string, sessionState jmap.SessionState) Resp } } +func (r *Request) noop(accountId string) Response { + return noContentResponse(single(accountId), r.session.State) +} + +func (r *Request) noopN(accountIds []string) Response { + return noContentResponse(accountIds, r.session.State) +} + func noContentResponseWithEtag(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { return Response{ accountIds: accountIds, @@ -115,6 +126,10 @@ func noContentResponseWithEtag(accountIds []string, sessionState jmap.SessionSta } } +func (r *Request) noContent(accountId string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return noContentResponseWithEtag(single(accountId), sessionState, objectType, etag) +} + /* func acceptedResponse(sessionState jmap.SessionState) Response { return Response{ @@ -139,18 +154,27 @@ func timeoutResponse(sessionState jmap.SessionState) Response { } */ -func notFoundResponse(accountIds []string, sessionState jmap.SessionState) Response { +func notFoundResponse(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { return Response{ accountIds: accountIds, body: nil, status: http.StatusNotFound, err: nil, - etag: "", + objectType: objectType, + etag: etag, sessionState: sessionState, } } -func etagNotFoundResponse(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { +func (r *Request) notFound(accountId string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return notFoundResponse(single(accountId), sessionState, objectType, etag) +} + +func (r *Request) notFoundN(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return notFoundResponse(accountIds, sessionState, objectType, etag) +} + +func etaggedNotFoundResponse(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { return Response{ accountIds: accountIds, body: nil, @@ -163,10 +187,21 @@ func etagNotFoundResponse(accountIds []string, sessionState jmap.SessionState, o } } -func notImplementedResponse() Response { +func (r *Request) etaggedNotFound(accountId string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { + return etaggedNotFoundResponse(single(accountId), sessionState, objectType, etag, jmap.Language(r.language())) +} + +func notImplementedResponse(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType) Response { return Response{ - body: nil, - status: http.StatusNotImplemented, - err: nil, + accountIds: accountIds, + body: nil, + status: http.StatusNotImplemented, + err: nil, + objectType: objectType, + sessionState: sessionState, } } + +func (r *Request) notImplementedN(accountIds []string, objectType ResponseObjectType) Response { + return notImplementedResponse(accountIds, r.session.State, objectType) +} diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go index 48ebe5b55b..f1e2c25117 100644 --- a/services/groupware/pkg/groupware/route.go +++ b/services/groupware/pkg/groupware/route.go @@ -23,7 +23,6 @@ const ( UriParamContactId = "contactid" // Identifier of the contact UriParamEventId = "eventid" // Idenfitier of the event UriParamBlobName = "blobname" - UriParamSince = "since" UriParamRole = "role" QueryParamMailboxSearchName = "name" QueryParamMailboxSearchRole = "role" @@ -102,8 +101,6 @@ func (g *Groupware) Route(r chi.Router) { r.Route("/{mailboxid}", func(r chi.Router) { r.Get("/", g.GetMailbox) r.Get("/emails", g.GetAllEmailsInMailbox) - r.Get("/emails/since/{since}", g.GetAllEmailsInMailboxSince) - r.Get("/changes", g.GetMailboxChanges) r.Patch("/", g.UpdateMailbox) r.Delete("/", g.DeleteMailbox) }) @@ -148,6 +145,7 @@ func (g *Groupware) Route(r chi.Router) { }) }) r.Route("/contacts", func(r chi.Router) { + r.Get("/", g.GetAllContacts) r.Post("/", g.CreateContact) r.Delete("/{contactid}", g.DeleteContact) r.Get("/{contactid}", g.GetContactById) @@ -170,6 +168,11 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/tasks", g.GetTasksInTaskList) }) }) + r.Route("/changes", func(r chi.Router) { + r.Get("/contacts", g.GetContactsChanges) + r.Get("/mailboxes", g.GetMailboxChanges) + r.Get("/emails", g.GetEmailChanges) + }) }) })