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