From 2883f044888a18341ebfd11e1ab79776fb481944 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Mon, 20 Apr 2026 10:17:14 +0200
Subject: [PATCH] groupware: refactoring using function templates
* adds creating addressbooks, calendars, mailboxes
* adds deleting mailbox, event, identity
* adds modifying an email
* introduce template functions for the Groupware API in templates.go,
and use those in route function implementations whenever possible
* add capability checking for mail, quota, blobs
* adds Changes interface
* adds JmapResponse interface
---
pkg/jmap/api_addressbook.go | 11 +-
pkg/jmap/api_blob.go | 18 +-
pkg/jmap/api_calendar.go | 11 +-
pkg/jmap/api_changes.go | 26 +-
pkg/jmap/api_contact.go | 22 +-
pkg/jmap/api_email.go | 61 ++-
pkg/jmap/api_event.go | 27 +-
pkg/jmap/api_identity.go | 27 +-
pkg/jmap/api_mailbox.go | 11 +-
pkg/jmap/api_quota.go | 11 +-
pkg/jmap/api_vacation.go | 17 +
pkg/jmap/export_test.go | 8 +
pkg/jmap/integration_calendar_test.go | 49 +-
pkg/jmap/integration_contact_test.go | 10 +-
pkg/jmap/integration_email_test.go | 20 +-
pkg/jmap/model.go | 80 ++-
pkg/jmap/model_examples.go | 4 +-
pkg/jmap/templates.go | 8 -
pkg/jmap/tools.go | 5 +
.../groupware/pkg/groupware/api_account.go | 12 +-
.../pkg/groupware/api_addressbooks.go | 170 +------
services/groupware/pkg/groupware/api_blob.go | 26 +-
.../groupware/pkg/groupware/api_calendars.go | 167 +------
.../groupware/pkg/groupware/api_changes.go | 4 +-
.../groupware/pkg/groupware/api_contacts.go | 184 +------
.../groupware/pkg/groupware/api_emails.go | 421 +++++-----------
.../groupware/pkg/groupware/api_events.go | 158 ++----
.../groupware/pkg/groupware/api_identity.go | 151 +-----
services/groupware/pkg/groupware/api_index.go | 2 +-
.../groupware/pkg/groupware/api_mailbox.go | 173 +------
.../groupware/pkg/groupware/api_objects.go | 2 +-
services/groupware/pkg/groupware/api_quota.go | 55 +-
.../groupware/pkg/groupware/api_tasklists.go | 6 +-
.../groupware/pkg/groupware/api_vacation.go | 38 +-
services/groupware/pkg/groupware/error.go | 73 ++-
services/groupware/pkg/groupware/framework.go | 11 +-
services/groupware/pkg/groupware/objtypes.go | 106 ++++
services/groupware/pkg/groupware/request.go | 116 ++++-
services/groupware/pkg/groupware/response.go | 8 +-
services/groupware/pkg/groupware/route.go | 34 +-
services/groupware/pkg/groupware/templates.go | 469 ++++++++++++++++++
services/groupware/pkg/groupware/tools.go | 20 +
42 files changed, 1326 insertions(+), 1506 deletions(-)
create mode 100644 services/groupware/pkg/groupware/objtypes.go
create mode 100644 services/groupware/pkg/groupware/templates.go
create mode 100644 services/groupware/pkg/groupware/tools.go
diff --git a/pkg/jmap/api_addressbook.go b/pkg/jmap/api_addressbook.go
index 9c1ee8d2ef..44d79ebc36 100644
--- a/pkg/jmap/api_addressbook.go
+++ b/pkg/jmap/api_addressbook.go
@@ -14,7 +14,16 @@ func (j *Client) GetAddressbooks(accountId string, ids []string, ctx Context) (A
)
}
-type AddressBookChanges = ChangesTemplate[AddressBook]
+type AddressBookChanges ChangesTemplate[AddressBook]
+
+var _ Changes[AddressBook] = AddressBookChanges{}
+
+func (c AddressBookChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c AddressBookChanges) GetOldState() State { return c.OldState }
+func (c AddressBookChanges) GetNewState() State { return c.NewState }
+func (c AddressBookChanges) GetCreated() []AddressBook { return c.Created }
+func (c AddressBookChanges) GetUpdated() []AddressBook { return c.Updated }
+func (c AddressBookChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Address Book changes since a given state.
// @apidoc addressbook,changes
diff --git a/pkg/jmap/api_blob.go b/pkg/jmap/api_blob.go
index 4024686422..b64b95f0e0 100644
--- a/pkg/jmap/api_blob.go
+++ b/pkg/jmap/api_blob.go
@@ -10,10 +10,10 @@ import (
var NS_BLOB = ns(JmapBlob)
-func (j *Client) GetBlobMetadata(accountId string, id string, ctx Context) (*Blob, SessionState, State, Language, Error) {
+func (j *Client) GetBlobMetadata(accountId string, ids []string, ctx Context) (BlobGetResponse, SessionState, State, Language, Error) {
get := BlobGetCommand{
AccountId: accountId,
- Ids: []string{id},
+ Ids: ids,
// add BlobPropertyData to retrieve the data
Properties: []string{BlobPropertyDigestSha256, BlobPropertyDigestSha512, BlobPropertySize},
}
@@ -21,22 +21,16 @@ func (j *Client) GetBlobMetadata(accountId string, id string, ctx Context) (*Blo
invocation(get, "0"),
)
if jerr != nil {
- return nil, "", "", "", jerr
+ return bail[BlobGetResponse](jerr)
}
- return command(j, ctx, cmd, func(body *Response) (*Blob, State, Error) {
+ return command(j, ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) {
var response BlobGetResponse
err := retrieveGet(ctx, body, get, "0", &response)
if err != nil {
- return nil, "", err
+ return BlobGetResponse{}, EmptyState, err
}
-
- if len(response.List) != 1 {
- ctx.Logger.Error().Msgf("%T.List has %v entries instead of 1", response, len(response.List))
- return nil, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
- }
- get := response.List[0]
- return &get, response.State, nil
+ return response, response.State, nil
})
}
diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go
index 536a34e7da..0aa62ef0a0 100644
--- a/pkg/jmap/api_calendar.go
+++ b/pkg/jmap/api_calendar.go
@@ -35,7 +35,16 @@ func (j *Client) GetCalendars(accountId string, ids []string, ctx Context) (Cale
)
}
-type CalendarChanges = ChangesTemplate[Calendar]
+type CalendarChanges ChangesTemplate[Calendar]
+
+var _ Changes[Calendar] = CalendarChanges{}
+
+func (c CalendarChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c CalendarChanges) GetOldState() State { return c.OldState }
+func (c CalendarChanges) GetNewState() State { return c.NewState }
+func (c CalendarChanges) GetCreated() []Calendar { return c.Created }
+func (c CalendarChanges) GetUpdated() []Calendar { return c.Updated }
+func (c CalendarChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Calendar changes since a given state.
// @apidoc calendar,changes
diff --git a/pkg/jmap/api_changes.go b/pkg/jmap/api_changes.go
index db2d715038..ab8f32b53c 100644
--- a/pkg/jmap/api_changes.go
+++ b/pkg/jmap/api_changes.go
@@ -10,7 +10,7 @@ import (
var NS_CHANGES = ns(JmapMail, JmapContacts, JmapCalendars) //, JmapQuota)
-type Changes struct {
+type ObjectChanges struct {
MaxChanges uint `json:"maxchanges,omitzero"`
Mailboxes *MailboxChangesResponse `json:"mailboxes,omitempty"`
Emails *EmailChangesResponse `json:"emails,omitempty"`
@@ -74,7 +74,7 @@ func (s StateMap) MarshalZerologObject(e *zerolog.Event) {
// Retrieve the changes in any type of objects at once since a given State.
// @api:tags changes
-func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint, ctx Context) (Changes, SessionState, State, Language, Error) { //NOSONAR
+func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint, ctx Context) (ObjectChanges, SessionState, State, Language, Error) { //NOSONAR
logger := log.From(j.logger("GetChanges", ctx).With().Object("state", stateMap).Uint("maxChanges", maxChanges))
ctx = ctx.WithLogger(logger)
@@ -107,18 +107,18 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
cmd, err := j.request(ctx, NS_CHANGES, methodCalls...)
if err != nil {
- return Changes{}, "", "", "", err
+ return ObjectChanges{}, "", "", "", err
}
- return command(j, ctx, cmd, func(body *Response) (Changes, State, Error) {
- changes := Changes{
+ return command(j, ctx, cmd, func(body *Response) (ObjectChanges, State, Error) {
+ changes := ObjectChanges{
MaxChanges: maxChanges,
}
states := map[string]State{}
var mailboxes MailboxChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandMailboxChanges, "mailboxes", &mailboxes); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Mailboxes = &mailboxes
states["mailbox"] = mailboxes.NewState
@@ -126,7 +126,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var emails EmailChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailChanges, "emails", &emails); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Emails = &emails
states["emails"] = emails.NewState
@@ -134,7 +134,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var calendars CalendarChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarChanges, "calendars", &calendars); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Calendars = &calendars
states["calendars"] = calendars.NewState
@@ -142,7 +142,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var events CalendarEventChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarEventChanges, "events", &events); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Events = &events
states["events"] = events.NewState
@@ -150,7 +150,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var addressbooks AddressBookChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandAddressBookChanges, "addressbooks", &addressbooks); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Addressbooks = &addressbooks
states["addressbooks"] = addressbooks.NewState
@@ -158,7 +158,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var contacts ContactCardChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandContactCardChanges, "contacts", &contacts); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Contacts = &contacts
states["contacts"] = contacts.NewState
@@ -166,7 +166,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var identities IdentityChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandIdentityChanges, "identities", &identities); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.Identities = &identities
states["identities"] = identities.NewState
@@ -174,7 +174,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var submissions EmailSubmissionChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailSubmissionChanges, "submissions", &submissions); err != nil {
- return Changes{}, "", err
+ return ObjectChanges{}, "", err
} else if ok {
changes.EmailSubmissions = &submissions
states["submissions"] = submissions.NewState
diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go
index d684978597..f16bb4eb25 100644
--- a/pkg/jmap/api_contact.go
+++ b/pkg/jmap/api_contact.go
@@ -1,7 +1,11 @@
package jmap
+import "github.com/opencloud-eu/opencloud/pkg/jscontact"
+
var NS_CONTACTS = ns(JmapContacts)
+var DEFAULT_CONTACT_CARD_VERSION = jscontact.JSContactVersion_1_0
+
func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Context) (ContactCardGetResponse, SessionState, State, Language, Error) {
return get(j, "GetContactCards", ContactCardType,
func(accountId string, ids []string) ContactCardGetCommand {
@@ -14,7 +18,16 @@ func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Cont
)
}
-type ContactCardChanges = ChangesTemplate[ContactCard]
+type ContactCardChanges ChangesTemplate[ContactCard]
+
+var _ Changes[ContactCard] = ContactCardChanges{}
+
+func (c ContactCardChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c ContactCardChanges) GetOldState() State { return c.OldState }
+func (c ContactCardChanges) GetNewState() State { return c.NewState }
+func (c ContactCardChanges) GetCreated() []ContactCard { return c.Created }
+func (c ContactCardChanges) GetUpdated() []ContactCard { return c.Updated }
+func (c ContactCardChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Contact Cards since a given State.
// @api:tags contact,changes
@@ -85,9 +98,12 @@ func (j *Client) QueryContactCards(accountIds []string,
)
}
-func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Context) (*ContactCard, SessionState, State, Language, Error) {
+func (j *Client) CreateContactCard(accountId string, contact ContactCardChange, ctx Context) (*ContactCard, SessionState, State, Language, Error) {
+ if contact.Version == nil {
+ contact.Version = &DEFAULT_CONTACT_CARD_VERSION
+ }
return create(j, "CreateContactCard", ContactCardType,
- func(accountId string, create map[string]ContactCard) ContactCardSetCommand {
+ func(accountId string, create map[string]ContactCardChange) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ids string) ContactCardGetCommand {
diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go
index 668823a4f0..a42c63b5a0 100644
--- a/pkg/jmap/api_email.go
+++ b/pkg/jmap/api_email.go
@@ -34,9 +34,9 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
methodCalls := []Invocation{invokeGet}
var markEmails EmailSetCommand
if markAsSeen {
- updates := make(map[string]EmailUpdate, len(ids))
+ updates := make(map[string]PatchObject, len(ids))
for _, id := range ids {
- updates[id] = EmailUpdate{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
+ updates[id] = PatchObject{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
}
markEmails = EmailSetCommand{AccountId: accountId, Update: updates}
methodCalls = []Invocation{invocation(markEmails, "0"), invokeGet}
@@ -111,7 +111,15 @@ func (j *Client) GetEmailBlobId(accountId string, id string, ctx Context) (strin
})
}
-type EmailSearchResults = SearchResultsTemplate[Email]
+type EmailSearchResults SearchResultsTemplate[Email]
+
+var _ SearchResults[Email] = EmailSearchResults{}
+
+func (r EmailSearchResults) GetResults() []Email { return r.Results }
+func (r EmailSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges }
+func (r EmailSearchResults) GetPosition() uint { return r.Position }
+func (r EmailSearchResults) GetLimit() uint { return r.Limit }
+func (r EmailSearchResults) GetTotal() *uint { return r.Total }
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR
@@ -200,14 +208,16 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS
})
}
-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"`
-}
+type EmailChanges ChangesTemplate[Email]
+
+var _ Changes[Email] = EmailChanges{}
+
+func (c EmailChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c EmailChanges) GetOldState() State { return c.OldState }
+func (c EmailChanges) GetNewState() State { return c.NewState }
+func (c EmailChanges) GetCreated() []Email { return c.Created }
+func (c EmailChanges) GetUpdated() []Email { return c.Updated }
+func (c EmailChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Emails since a given State.
// @api:tags email,changes
@@ -495,13 +505,13 @@ type EmailWithSnippets struct {
type EmailQueryWithSnippetsResult struct {
Results []EmailWithSnippets `json:"results"`
Total uint `json:"total"`
+ Position uint `json:"position"`
Limit uint `json:"limit,omitzero"`
- Position uint `json:"position,omitzero"`
QueryState State `json:"queryState"`
}
func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR
- filter EmailFilterElement, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint,
+ filter EmailFilterElement, offset int, limit uint, collapseThreads bool, calculateTotal bool, fetchBodies bool, maxBodyValueBytes uint,
ctx Context) (map[string]EmailQueryWithSnippetsResult, SessionState, State, Language, Error) {
logger := j.loggerParams("QueryEmailsWithSnippets", ctx, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
@@ -515,8 +525,8 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR
AccountId: accountId,
Filter: filter,
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
- CollapseThreads: false,
- CalculateTotal: true,
+ CollapseThreads: collapseThreads,
+ CalculateTotal: calculateTotal,
}
if offset > 0 {
query.Position = offset
@@ -689,10 +699,10 @@ func (j *Client) ImportEmail(accountId string, data []byte, ctx Context) (Upload
}
-func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId string, ctx Context) (*Email, SessionState, State, Language, Error) {
+func (j *Client) CreateEmail(accountId string, email EmailChange, replaceId string, ctx Context) (*Email, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
- Create: map[string]EmailCreate{
+ Create: map[string]EmailChange{
"c": email,
},
}
@@ -744,7 +754,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri
// To create drafts, use the CreateEmail function instead.
//
// To delete mails, use the DeleteEmails function instead.
-func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, ctx Context) (map[string]*Email, SessionState, State, Language, Error) {
+func (j *Client) UpdateEmails(accountId string, updates map[string]PatchObject, ctx Context) (map[string]*Email, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
Update: updates,
@@ -770,6 +780,21 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
})
}
+func (j *Client) UpdateEmail(accountId string, id string, changes EmailChange, ctx Context) (Email, SessionState, State, Language, Error) {
+ return update(j, "UpdateEmail", EmailType,
+ func(update map[string]PatchObject) EmailSetCommand {
+ return EmailSetCommand{AccountId: accountId, Update: update}
+ },
+ func(id string) EmailGetCommand {
+ return EmailGetCommand{AccountId: accountId, Ids: []string{id}}
+ },
+ func(resp EmailSetResponse) map[string]SetError { return resp.NotUpdated },
+ func(resp EmailGetResponse) Email { return resp.List[0] },
+ id, changes,
+ ctx,
+ )
+}
+
func (j *Client) DeleteEmails(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteEmails", EmailType,
func(accountId string, destroy []string) EmailSetCommand {
diff --git a/pkg/jmap/api_event.go b/pkg/jmap/api_event.go
index 1fabadbe14..709938d014 100644
--- a/pkg/jmap/api_event.go
+++ b/pkg/jmap/api_event.go
@@ -12,6 +12,18 @@ func (r CalendarEventSearchResults) GetPosition() uint { return r.Pos
func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit }
func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total }
+func (j *Client) GetCalendarEvents(accountId string, eventIds []string, ctx Context) (CalendarEventGetResponse, SessionState, State, Language, Error) {
+ return get(j, "GetCalendarEvents", CalendarEventType,
+ func(accountId string, ids []string) CalendarEventGetCommand {
+ return CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}
+ },
+ CalendarEventGetResponse{},
+ identity1,
+ accountId, eventIds,
+ ctx,
+ )
+}
+
func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position int, limit uint, calculateTotal bool,
@@ -38,7 +50,16 @@ func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
)
}
-type CalendarEventChanges = ChangesTemplate[CalendarEvent]
+type CalendarEventChanges ChangesTemplate[CalendarEvent]
+
+var _ Changes[CalendarEvent] = CalendarEventChanges{}
+
+func (c CalendarEventChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c CalendarEventChanges) GetOldState() State { return c.OldState }
+func (c CalendarEventChanges) GetNewState() State { return c.NewState }
+func (c CalendarEventChanges) GetCreated() []CalendarEvent { return c.Created }
+func (c CalendarEventChanges) GetUpdated() []CalendarEvent { return c.Updated }
+func (c CalendarEventChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Calendar Events since a given State.
// @api:tags event,changes
@@ -74,9 +95,9 @@ func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, max
)
}
-func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
+func (j *Client) CreateCalendarEvent(accountId string, event CalendarEventChange, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
return create(j, "CreateCalendarEvent", CalendarEventType,
- func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand {
+ func(accountId string, create map[string]CalendarEventChange) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ref string) CalendarEventGetCommand {
diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go
index e510de4e75..7237a8f796 100644
--- a/pkg/jmap/api_identity.go
+++ b/pkg/jmap/api_identity.go
@@ -8,23 +8,13 @@ import (
var NS_IDENTITY = ns(JmapMail)
-func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
- return getA(j, "GetAllIdentities", IdentityType,
- func(accountId string, ids []string) IdentityGetCommand {
- return IdentityGetCommand{AccountId: accountId}
- },
- IdentityGetResponse{},
- accountId, []string{},
- ctx,
- )
-}
-
-func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
- return getA(j, "GetIdentities", IdentityType,
+func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) (IdentityGetResponse, SessionState, State, Language, Error) {
+ return get(j, "GetIdentities", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: ids}
},
IdentityGetResponse{},
+ identity1,
accountId, identityIds,
ctx,
)
@@ -140,7 +130,16 @@ func (j *Client) DeleteIdentity(accountId string, destroyIds []string, ctx Conte
)
}
-type IdentityChanges = ChangesTemplate[Identity]
+type IdentityChanges ChangesTemplate[Identity]
+
+var _ Changes[Identity] = IdentityChanges{}
+
+func (c IdentityChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c IdentityChanges) GetOldState() State { return c.OldState }
+func (c IdentityChanges) GetNewState() State { return c.NewState }
+func (c IdentityChanges) GetCreated() []Identity { return c.Created }
+func (c IdentityChanges) GetUpdated() []Identity { return c.Updated }
+func (c IdentityChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Email Identities since a given State.
// @api:tags email,changes
diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go
index fc26b5a377..eae723c5c9 100644
--- a/pkg/jmap/api_mailbox.go
+++ b/pkg/jmap/api_mailbox.go
@@ -113,7 +113,16 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, roles []string, ct
})
}
-type MailboxChanges = ChangesTemplate[Mailbox]
+type MailboxChanges ChangesTemplate[Mailbox]
+
+var _ Changes[Mailbox] = MailboxChanges{}
+
+func (c MailboxChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c MailboxChanges) GetOldState() State { return c.OldState }
+func (c MailboxChanges) GetNewState() State { return c.NewState }
+func (c MailboxChanges) GetCreated() []Mailbox { return c.Created }
+func (c MailboxChanges) GetUpdated() []Mailbox { return c.Updated }
+func (c MailboxChanges) GetDestroyed() []string { return c.Destroyed }
func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
return MailboxChanges{
diff --git a/pkg/jmap/api_quota.go b/pkg/jmap/api_quota.go
index 0e84d562a8..4b56b87f23 100644
--- a/pkg/jmap/api_quota.go
+++ b/pkg/jmap/api_quota.go
@@ -15,7 +15,16 @@ func (j *Client) GetQuotas(accountIds []string, ctx Context) (map[string]QuotaGe
)
}
-type QuotaChanges = ChangesTemplate[Quota]
+type QuotaChanges ChangesTemplate[Quota]
+
+var _ Changes[Quota] = QuotaChanges{}
+
+func (c QuotaChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c QuotaChanges) GetOldState() State { return c.OldState }
+func (c QuotaChanges) GetNewState() State { return c.NewState }
+func (c QuotaChanges) GetCreated() []Quota { return c.Created }
+func (c QuotaChanges) GetUpdated() []Quota { return c.Updated }
+func (c QuotaChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Quotas since a given State.
// @api:tags quota,changes
diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go
index 30625a04d0..40ae7b2d59 100644
--- a/pkg/jmap/api_vacation.go
+++ b/pkg/jmap/api_vacation.go
@@ -47,6 +47,23 @@ type VacationResponseChange struct {
HtmlBody string `json:"htmlBody,omitempty"`
}
+var _ Change = VacationResponseChange{}
+
+func (m VacationResponseChange) AsPatch() (PatchObject, error) {
+ return toPatchObject(m)
+}
+
+type VacationResponseChanges ChangesTemplate[VacationResponse]
+
+var _ Changes[VacationResponse] = VacationResponseChanges{}
+
+func (c VacationResponseChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c VacationResponseChanges) GetOldState() State { return c.OldState }
+func (c VacationResponseChanges) GetNewState() State { return c.NewState }
+func (c VacationResponseChanges) GetCreated() []VacationResponse { return c.Created }
+func (c VacationResponseChanges) GetUpdated() []VacationResponse { return c.Updated }
+func (c VacationResponseChanges) GetDestroyed() []string { return c.Destroyed }
+
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponseChange,
ctx Context) (VacationResponse, SessionState, State, Language, Error) {
logger := j.logger("SetVacationResponse", ctx)
diff --git a/pkg/jmap/export_test.go b/pkg/jmap/export_test.go
index bf61f4263c..1b1a747173 100644
--- a/pkg/jmap/export_test.go
+++ b/pkg/jmap/export_test.go
@@ -79,3 +79,11 @@ func parseConsts(pkgID string, suffix string, typeName string) (map[string]strin
}
return result, nil
}
+
+func firstKey[K comparable, V any](m map[K]V) (K, bool) {
+ for k := range m {
+ return k, true
+ }
+ var zero K
+ return zero, false
+}
diff --git a/pkg/jmap/integration_calendar_test.go b/pkg/jmap/integration_calendar_test.go
index 51b2dab0b5..3ed6b9ea0b 100644
--- a/pkg/jmap/integration_calendar_test.go
+++ b/pkg/jmap/integration_calendar_test.go
@@ -422,38 +422,37 @@ func (s *StalwartTest) fillEvents( //NOSONAR
keywords := pickKeywords()
categories := pickCategories()
- sequence := 0
+ sequence := uint(0)
alertId := id()
alertOffset := pickRandom("-PT5M", "-PT10M", "-PT15M")
- obj := CalendarEvent{
- Id: "",
+ obj := CalendarEventChange{
CalendarIds: toBoolMapS(calendarId),
- IsDraft: isDraft,
- Event: jscalendar.Event{
+ IsDraft: &isDraft,
+ EventChange: jscalendar.EventChange{
Type: jscalendar.EventType,
Start: jscalendar.LocalDateTime(start),
- Duration: jscalendar.Duration(duration),
- Status: status,
- Object: jscalendar.Object{
- CommonObject: jscalendar.CommonObject{
- Uid: uid,
- ProdId: productName,
- Title: title,
- Description: description,
- DescriptionContentType: descriptionFormat,
- Locale: locale,
- Color: color,
+ Duration: ptr(jscalendar.Duration(duration)),
+ Status: &status,
+ ObjectChange: jscalendar.ObjectChange{
+ CommonObjectChange: jscalendar.CommonObjectChange{
+ Uid: &uid,
+ ProdId: &productName,
+ Title: &title,
+ Description: &description,
+ DescriptionContentType: &descriptionFormat,
+ Locale: &locale,
+ Color: &color,
},
- Sequence: uint(sequence),
- ShowWithoutTime: false,
- FreeBusyStatus: freeBusy,
- Privacy: privacy,
+ Sequence: uintPtr(sequence),
+ ShowWithoutTime: boolPtr(false),
+ FreeBusyStatus: &freeBusy,
+ Privacy: &privacy,
SentBy: organizerEmail,
Participants: participantObjs,
- TimeZone: tz,
- HideAttendees: false,
+ TimeZone: &tz,
+ HideAttendees: boolPtr(false),
ReplyTo: map[jscalendar.ReplyMethod]string{
jscalendar.ReplyMethodImip: "mailto:" + organizerEmail, //NOSONAR
},
@@ -476,8 +475,8 @@ func (s *StalwartTest) fillEvents( //NOSONAR
}
if EnableEventMayInviteFields {
- obj.MayInviteSelf = true
- obj.MayInviteOthers = true
+ obj.MayInviteSelf = boolPtr(true)
+ obj.MayInviteOthers = boolPtr(true)
boxes.mayInvite = true
}
@@ -492,7 +491,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
}
if mainLocationId != "" {
- obj.MainLocationId = mainLocationId
+ obj.MainLocationId = &mainLocationId
}
err = propmap(i%2 == 0, 1, 1, &obj.Links, func(int, string) (jscalendar.Link, error) {
diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_contact_test.go
index 6ae7236ccf..9879dadd95 100644
--- a/pkg/jmap/integration_contact_test.go
+++ b/pkg/jmap/integration_contact_test.go
@@ -332,13 +332,13 @@ func (s *StalwartTest) fillContacts( //NOSONAR
nameObj := createName(person)
language := pickLanguage()
- card := ContactCard{
+ card := ContactCardChange{
Type: jscontact.ContactCardType,
- Version: "1.0",
+ Version: ptr(jscontact.JSContactVersion_1_0),
AddressBookIds: toBoolMap([]string{addressbookId}),
- ProdId: productName,
- Language: language,
- Kind: jscontact.ContactCardKindIndividual,
+ ProdId: &productName,
+ Language: &language,
+ Kind: ptr(jscontact.ContactCardKindIndividual),
Name: &nameObj,
}
diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go
index 74aa484fd7..b1d1149169 100644
--- a/pkg/jmap/integration_email_test.go
+++ b/pkg/jmap/integration_email_test.go
@@ -56,12 +56,12 @@ func TestEmails(t *testing.T) {
{
{
- resp, sessionState, _, _, err := s.client.GetAllIdentities(accountId, ctx)
+ resp, sessionState, _, _, err := s.client.GetIdentities(accountId, []string{}, ctx)
require.NoError(err)
require.Equal(session.State, sessionState)
- require.Len(resp, 1)
- require.Equal(user.email, resp[0].Email)
- require.Equal(user.description, resp[0].Name)
+ require.Len(resp.List, 1)
+ require.Equal(user.email, resp.List[0].Email)
+ require.Equal(user.description, resp.List[0].Name)
}
{
@@ -188,13 +188,13 @@ func TestSendingEmails(t *testing.T) {
{
var identity Identity
{
- identities, _, _, _, err := s.client.GetAllIdentities(accountId, ctx)
+ resp, _, _, _, err := s.client.GetIdentities(accountId, []string{}, ctx)
require.NoError(err)
- require.NotEmpty(identities)
- identity = identities[0]
+ require.NotEmpty(resp.List)
+ identity = resp.List[0]
}
- create := EmailCreate{
+ create := EmailChange{
Keywords: toBoolMapS("test"),
Subject: subject,
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
@@ -214,7 +214,7 @@ func TestSendingEmails(t *testing.T) {
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
}
- update := EmailCreate{
+ update := EmailChange{
From: []EmailAddress{{Name: fromName, Email: from.email}},
To: []EmailAddress{{Name: to.description, Email: to.email}},
Cc: []EmailAddress{{Name: cc.description, Email: cc.email}},
@@ -240,7 +240,7 @@ func TestSendingEmails(t *testing.T) {
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
require.Equal(notFound[0], created.Id)
var ok bool
- updatedMailboxId, ok = structs.FirstKey(email.MailboxIds)
+ updatedMailboxId, ok = firstKey(email.MailboxIds)
require.True(ok)
}
diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go
index 075b08ee0a..a454bb9008 100644
--- a/pkg/jmap/model.go
+++ b/pkg/jmap/model.go
@@ -2,6 +2,7 @@ package jmap
import (
"encoding/json"
+ "fmt"
"io"
"time"
@@ -1326,12 +1327,17 @@ type JmapCommand interface {
GetObjectType() ObjectType
}
+type JmapResponse[T Foo] interface {
+ GetMarker() T
+}
+
type GetCommand[T Foo] interface {
JmapCommand
GetResponse() GetResponse[T]
}
type GetResponse[T Foo] interface {
+ JmapResponse[T]
GetState() State
GetNotFound() []string
GetList() []T
@@ -1343,12 +1349,12 @@ type SetCommand[T Foo] interface {
}
type SetResponse[T Foo] interface {
+ JmapResponse[T]
GetNotCreated() map[string]SetError
GetNotUpdated() map[string]SetError
GetNotDestroyed() map[string]SetError
GetOldState() State
GetNewState() State
- GetMarker() T
}
type Change interface {
@@ -1361,13 +1367,13 @@ type ChangesCommand[T Foo] interface {
}
type ChangesResponse[T Foo] interface {
+ JmapResponse[T]
GetOldState() State
GetNewState() State
GetHasMoreChanges() bool
GetCreated() []string
GetUpdated() []string
GetDestroyed() []string
- GetMarker() T
}
type QueryCommand[T Foo] interface {
@@ -1376,8 +1382,8 @@ type QueryCommand[T Foo] interface {
}
type QueryResponse[T Foo] interface {
+ JmapResponse[T]
GetQueryState() State
- GetMarker() T
}
type UploadCommand[T Foo] interface {
@@ -1386,7 +1392,7 @@ type UploadCommand[T Foo] interface {
}
type UploadResponse[T Foo] interface {
- GetMarker() T
+ JmapResponse[T]
}
type ParseCommand[T Foo] interface {
@@ -1395,7 +1401,7 @@ type ParseCommand[T Foo] interface {
}
type ParseResponse[T Foo] interface {
- GetMarker() T
+ JmapResponse[T]
}
type ChangesTemplate[T Foo] struct {
@@ -1407,6 +1413,15 @@ type ChangesTemplate[T Foo] struct {
Destroyed []string `json:"destroyed,omitempty"`
}
+type Changes[T Foo] interface {
+ GetHasMoreChanges() bool
+ GetOldState() State
+ GetNewState() State
+ GetCreated() []T
+ GetUpdated() []T
+ GetDestroyed() []string
+}
+
type SearchResultsTemplate[T Foo] struct {
Results []T `json:"results"`
CanCalculateChanges bool `json:"canCalculateChanges"`
@@ -2965,6 +2980,7 @@ type EmailSubmissionGetResponse struct {
var _ GetResponse[EmailSubmission] = &EmailSubmissionGetResponse{}
+func (r EmailSubmissionGetResponse) GetMarker() EmailSubmission { return EmailSubmission{} }
func (r EmailSubmissionGetResponse) GetState() State { return r.State }
func (r EmailSubmissionGetResponse) GetNotFound() []string { return r.NotFound }
func (r EmailSubmissionGetResponse) GetList() []EmailSubmission { return r.List }
@@ -3268,6 +3284,7 @@ type MailboxGetResponse struct {
var _ GetResponse[Mailbox] = &MailboxGetResponse{}
+func (r MailboxGetResponse) GetMarker() Mailbox { return Mailbox{} }
func (r MailboxGetResponse) GetState() State { return r.State }
func (r MailboxGetResponse) GetNotFound() []string { return r.NotFound }
func (r MailboxGetResponse) GetList() []Mailbox { return r.List }
@@ -3396,7 +3413,7 @@ var _ QueryResponse[Mailbox] = &MailboxQueryResponse{}
func (r MailboxQueryResponse) GetQueryState() State { return r.QueryState }
func (r MailboxQueryResponse) GetMarker() Mailbox { return Mailbox{} }
-type EmailCreate struct {
+type EmailChange struct {
// The set of Mailbox ids this Email belongs to.
//
// An Email in the mail store MUST belong to one or more Mailboxes at all times
@@ -3498,7 +3515,11 @@ type EmailCreate struct {
Attachments []EmailBodyPart `json:"attachments,omitempty"`
}
-type EmailUpdate map[string]any
+var _ Change = EmailChange{}
+
+func (e EmailChange) AsPatch() (PatchObject, error) {
+ return toPatchObject(e)
+}
type EmailSetCommand struct {
// The id of the account to use.
@@ -3520,7 +3541,7 @@ type EmailSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
- Create map[string]EmailCreate `json:"create,omitempty"`
+ Create map[string]EmailChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
@@ -3547,7 +3568,7 @@ type EmailSetCommand struct {
//
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
// processes it the same either way.
- Update map[string]EmailUpdate `json:"update,omitempty"`
+ Update map[string]PatchObject `json:"update,omitempty"`
// A list of ids for Email objects to permanently delete, or null if no objects are to be destroyed.
Destroy []string `json:"destroy,omitempty"`
@@ -3966,6 +3987,7 @@ type IdentityGetResponse struct {
var _ GetResponse[Identity] = &IdentityGetResponse{}
+func (r IdentityGetResponse) GetMarker() Identity { return Identity{} }
func (r IdentityGetResponse) GetState() State { return r.State }
func (r IdentityGetResponse) GetNotFound() []string { return r.NotFound }
func (r IdentityGetResponse) GetList() []Identity { return r.List }
@@ -4050,6 +4072,7 @@ type VacationResponseGetResponse struct {
var _ GetResponse[VacationResponse] = &VacationResponseGetResponse{}
+func (r VacationResponseGetResponse) GetMarker() VacationResponse { return VacationResponse{} }
func (r VacationResponseGetResponse) GetState() State { return r.State }
func (r VacationResponseGetResponse) GetNotFound() []string { return r.NotFound }
func (r VacationResponseGetResponse) GetList() []VacationResponse { return r.List }
@@ -4195,6 +4218,26 @@ var _ Idable = &Blob{}
func (f Blob) GetObjectType() ObjectType { return BlobType }
func (f Blob) GetId() string { return f.Id }
+type BlobChange struct {
+}
+
+var _ Change = BlobChange{}
+
+func (m BlobChange) AsPatch() (PatchObject, error) {
+ return nil, fmt.Errorf("BlobChange is unsupported")
+}
+
+type BlobChanges ChangesTemplate[Blob]
+
+var _ Changes[Blob] = BlobChanges{}
+
+func (c BlobChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
+func (c BlobChanges) GetOldState() State { return c.OldState }
+func (c BlobChanges) GetNewState() State { return c.NewState }
+func (c BlobChanges) GetCreated() []Blob { return c.Created }
+func (c BlobChanges) GetUpdated() []Blob { return c.Updated }
+func (c BlobChanges) GetDestroyed() []string { return c.Destroyed }
+
type BlobGetCommand struct {
AccountId string `json:"accountId"`
Ids []string `json:"ids,omitempty"`
@@ -4255,6 +4298,7 @@ type BlobGetResponse struct {
var _ GetResponse[Blob] = &BlobGetResponse{}
+func (r BlobGetResponse) GetMarker() Blob { return Blob{} }
func (r BlobGetResponse) GetState() State { return r.State }
func (r BlobGetResponse) GetNotFound() []string { return r.NotFound }
func (r BlobGetResponse) GetList() []Blob { return r.List }
@@ -6370,6 +6414,15 @@ var _ Idable = &Quota{}
func (f Quota) GetObjectType() ObjectType { return QuotaType }
func (f Quota) GetId() string { return f.Id }
+type QuotaChange struct {
+}
+
+var _ Change = QuotaChange{}
+
+func (m QuotaChange) AsPatch() (PatchObject, error) {
+ return nil, fmt.Errorf("QuotaChange is unsupported")
+}
+
// See [RFC8098] for the exact meaning of these different fields.
//
// These fields are defined as case insensitive in [RFC8098] but are case sensitive in this RFC
@@ -6492,6 +6545,7 @@ type QuotaGetResponse struct {
var _ GetResponse[Quota] = &QuotaGetResponse{}
+func (r QuotaGetResponse) GetMarker() Quota { return Quota{} }
func (r QuotaGetResponse) GetState() State { return r.State }
func (r QuotaGetResponse) GetNotFound() []string { return r.NotFound }
func (r QuotaGetResponse) GetList() []Quota { return r.List }
@@ -6593,6 +6647,7 @@ type AddressBookGetResponse struct {
var _ GetResponse[AddressBook] = &AddressBookGetResponse{}
+func (r AddressBookGetResponse) GetMarker() AddressBook { return AddressBook{} }
func (r AddressBookGetResponse) GetState() State { return r.State }
func (r AddressBookGetResponse) GetNotFound() []string { return r.NotFound }
func (r AddressBookGetResponse) GetList() []AddressBook { return r.List }
@@ -7168,6 +7223,7 @@ type ContactCardGetResponse struct {
var _ GetResponse[ContactCard] = &ContactCardGetResponse{}
+func (r ContactCardGetResponse) GetMarker() ContactCard { return ContactCard{} }
func (r ContactCardGetResponse) GetState() State { return r.State }
func (r ContactCardGetResponse) GetNotFound() []string { return r.NotFound }
func (r ContactCardGetResponse) GetList() []ContactCard { return r.List }
@@ -7251,7 +7307,7 @@ type ContactCardSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
- Create map[string]ContactCard `json:"create,omitempty"`
+ Create map[string]ContactCardChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
@@ -7420,6 +7476,7 @@ type CalendarGetResponse struct {
var _ GetResponse[Calendar] = &CalendarGetResponse{}
+func (r CalendarGetResponse) GetMarker() Calendar { return Calendar{} }
func (r CalendarGetResponse) GetState() State { return r.State }
func (r CalendarGetResponse) GetNotFound() []string { return r.NotFound }
func (r CalendarGetResponse) GetList() []Calendar { return r.List }
@@ -7896,6 +7953,7 @@ type CalendarEventGetResponse struct {
var _ GetResponse[CalendarEvent] = &CalendarEventGetResponse{}
+func (r CalendarEventGetResponse) GetMarker() CalendarEvent { return CalendarEvent{} }
func (r CalendarEventGetResponse) GetState() State { return r.State }
func (r CalendarEventGetResponse) GetNotFound() []string { return r.NotFound }
func (r CalendarEventGetResponse) GetList() []CalendarEvent { return r.List }
@@ -7985,7 +8043,7 @@ type CalendarEventSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
- Create map[string]CalendarEvent `json:"create,omitempty"`
+ Create map[string]CalendarEventChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go
index dec9f8020f..9adb6e3641 100644
--- a/pkg/jmap/model_examples.go
+++ b/pkg/jmap/model_examples.go
@@ -2058,8 +2058,8 @@ func (e Exemplar) EmailChanges() EmailChanges {
}
}
-func (e Exemplar) Changes() (Changes, string, string) {
- return Changes{
+func (e Exemplar) Changes() (ObjectChanges, string, string) {
+ return ObjectChanges{
MaxChanges: 3,
Mailboxes: &MailboxChangesResponse{
AccountId: e.AccountId,
diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go
index 5d97a23b31..4397c6bec9 100644
--- a/pkg/jmap/templates.go
+++ b/pkg/jmap/templates.go
@@ -35,14 +35,6 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSON
})
}
-func getA[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T]]( //NOSONAR
- client *Client, name string, objType ObjectType,
- getCommandFactory func(string, []string) GETREQ,
- resp GETRESP,
- accountId string, ids []string, ctx Context) ([]T, SessionState, State, Language, Error) {
- return get(client, name, objType, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, ids, ctx)
-}
-
func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go
index 06ac08a701..0f7e206772 100644
--- a/pkg/jmap/tools.go
+++ b/pkg/jmap/tools.go
@@ -50,6 +50,11 @@ func mcid(accountId string, tag string) string {
return accountId + ":" + tag
}
+func bail[R JmapResponse[T], T Foo](err Error) (R, SessionState, State, Language, Error) {
+ var zero R
+ return zero, EmptySessionState, EmptyState, NoLanguage, err
+}
+
type Cmdr interface {
ApiSupplier
Hooks
diff --git a/services/groupware/pkg/groupware/api_account.go b/services/groupware/pkg/groupware/api_account.go
index 0314bf1881..84873a949e 100644
--- a/services/groupware/pkg/groupware/api_account.go
+++ b/services/groupware/pkg/groupware/api_account.go
@@ -10,14 +10,14 @@ import (
)
// Get attributes of a given account.
-func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
+func (g *Groupware) GetAccountById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, account, err := req.GetAccountForMail()
if err != nil {
return req.error(accountId, err)
}
var body jmap.Account = account
- return req.respond(accountId, body, req.session.State, AccountResponseObjectType, "")
+ return req.respond(accountId, body, req.session.State, AccountResponseObjectType, jmap.EmptyState, jmap.NoLanguage)
})
}
@@ -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 req.respondN(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, "")
+ return req.respondN(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.EmptyState, jmap.NoLanguage)
})
}
@@ -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 req.respondN(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state)
+ return req.respondN(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang)
})
}
@@ -76,7 +76,7 @@ type AccountWithId struct {
}
type AccountWithIdAndIdentities struct {
- AccountId string `json:"accountId,omitempty"`
- jmap.Account
+ AccountId string `json:"accountId,omitempty"`
Identities []jmap.Identity `json:"identities,omitempty"`
+ jmap.Account
}
diff --git a/services/groupware/pkg/groupware/api_addressbooks.go b/services/groupware/pkg/groupware/api_addressbooks.go
index e54e4c190b..98a1cf0875 100644
--- a/services/groupware/pkg/groupware/api_addressbooks.go
+++ b/services/groupware/pkg/groupware/api_addressbooks.go
@@ -2,188 +2,32 @@ package groupware
import (
"net/http"
-
- "github.com/opencloud-eu/opencloud/pkg/jmap"
- "github.com/opencloud-eu/opencloud/pkg/log"
)
// Get all addressbooks of an account.
func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needContactWithAccount()
- if !ok {
- return resp
- }
-
- addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, []string{}, req.ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- var body jmap.AddressBookGetResponse = addressbooks
- return req.respond(accountId, body, sessionState, AddressBookResponseObjectType, state)
- })
+ getall(AddressBook, w, r, g, g.jmap.GetAddressbooks)
}
// Get an addressbook of an account by its identifier.
-func (g *Groupware) GetAddressbook(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()
-
- addressBookId, err := req.PathParam(UriParamAddressBookId)
- if err != nil {
- 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, []string{addressBookId}, req.ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- switch len(addressbooks.List) {
- case 0:
- return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
- case 1:
- return req.respond(accountId, addressbooks.List[0], sessionState, ContactResponseObjectType, state)
- default:
- logger.Error().Msgf("found %d addressbooks matching '%s' instead of 1", len(addressbooks.List), addressBookId)
- return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
- }
- })
+func (g *Groupware) GetAddressbookById(w http.ResponseWriter, r *http.Request) {
+ get(AddressBook, w, r, g, g.jmap.GetAddressbooks)
}
// Get the changes to Address Books since a certain State.
// @api:tags addressbook,changes
func (g *Groupware) GetAddressBookChanges(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()
-
- maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Uint(QueryParamMaxChanges, maxChanges)
- }
-
- sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list addressbook changes"))
- if sinceState != "" {
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetAddressbookChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, changes, sessionState, AddressBookResponseObjectType, state)
- })
+ changes(AddressBook, w, r, g, g.jmap.GetAddressbookChanges)
}
func (g *Groupware) CreateAddressBook(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 create jmap.AddressBookChange
- err := req.bodydoc(&create, "The address book to create")
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- created, sessionState, state, lang, jerr := g.jmap.CreateAddressBook(accountId, create, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
- })
+ create(AddressBook, w, r, g, nil, g.jmap.CreateAddressBook)
}
func (g *Groupware) DeleteAddressBook(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().Str(accountId, log.SafeString(accountId))
-
- addressBookId, err := req.PathParam(UriParamAddressBookId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- deleted, sessionState, state, lang, jerr := g.jmap.DeleteAddressBook(accountId, []string{addressBookId}, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- for _, e := range deleted {
- desc := e.Description
- if desc != "" {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteAddressBook,
- withDetail(e.Description),
- ))
- } else {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteAddressBook,
- ))
- }
- }
- return req.noContent(accountId, sessionState, AddressBookResponseObjectType, state)
- })
+ delete(AddressBook, w, r, g, g.jmap.DeleteAddressBook)
}
func (g *Groupware) ModifyAddressBook(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().Str(accountId, log.SafeString(accountId))
- id, err := req.PathParamDoc(UriParamAddressBookId, "The unique identifier of the AddressBook to modify")
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamAddressBookId, log.SafeString(id))
-
- var change jmap.AddressBookChange
- err = req.body(&change)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- updated, sessionState, state, lang, jerr := g.jmap.UpdateAddressBook(accountId, id, change, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, updated, sessionState, AddressBookResponseObjectType, state)
- })
+ modify(AddressBook, w, r, g, g.jmap.UpdateAddressBook)
}
diff --git a/services/groupware/pkg/groupware/api_blob.go b/services/groupware/pkg/groupware/api_blob.go
index 133cd9c5ec..0815305a2e 100644
--- a/services/groupware/pkg/groupware/api_blob.go
+++ b/services/groupware/pkg/groupware/api_blob.go
@@ -14,31 +14,7 @@ const (
)
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 req.error(accountId, err)
- }
- l := req.logger.With().Str(logAccountId, accountId)
-
- blobId, err := req.PathParam(UriParamBlobId)
- if err != nil {
- return req.error(accountId, err)
- }
- l = l.Str(UriParamBlobId, blobId)
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- res, sessionState, state, lang, jerr := g.jmap.GetBlobMetadata(accountId, blobId, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- if res == nil {
- return req.notFound(accountId, sessionState, BlobResponseObjectType, state)
- }
- return req.respond(accountId, res, sessionState, BlobResponseObjectType, state)
- })
+ get(Blob, w, r, g, g.jmap.GetBlobMetadata)
}
func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
diff --git a/services/groupware/pkg/groupware/api_calendars.go b/services/groupware/pkg/groupware/api_calendars.go
index 02f9dc874e..76e4b07c1b 100644
--- a/services/groupware/pkg/groupware/api_calendars.go
+++ b/services/groupware/pkg/groupware/api_calendars.go
@@ -2,187 +2,32 @@ package groupware
import (
"net/http"
-
- "github.com/opencloud-eu/opencloud/pkg/jmap"
- "github.com/opencloud-eu/opencloud/pkg/log"
)
// Get all calendars of an account.
func (g *Groupware) GetCalendars(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
-
- calendars, sessionState, state, lang, jerr := g.jmap.GetCalendars(accountId, nil, req.ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, calendars, sessionState, CalendarResponseObjectType, state)
- })
+ getall(Calendar, w, r, g, g.jmap.GetCalendars)
}
// Get a calendar of an account by its identifier.
func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
-
- l := req.logger.With()
-
- calendarId, err := req.PathParam(UriParamCalendarId)
- if err != nil {
- 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, single(calendarId), req.ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- switch len(calendars.List) {
- case 0:
- return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
- case 1:
- return req.respond(accountId, calendars.List[0], sessionState, ContactResponseObjectType, state)
- default:
- logger.Error().Msgf("found %d calendars matching '%s' instead of 1", len(calendars.List), calendarId)
- return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
- }
- })
+ get(Calendar, w, r, g, g.jmap.GetCalendars)
}
// Get the changes to Calendars since a certain State.
// @api:tags calendar,changes
func (g *Groupware) GetCalendarChanges(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
-
- l := req.logger.With()
-
- maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Uint(QueryParamMaxChanges, maxChanges)
- }
-
- sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list calendar changes"))
- if sinceState != "" {
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetCalendarChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, changes, sessionState, CalendarResponseObjectType, state)
- })
+ changes(Calendar, w, r, g, g.jmap.GetCalendarChanges)
}
func (g *Groupware) CreateCalendar(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
-
- l := req.logger.With()
-
- var create jmap.CalendarChange
- err := req.bodydoc(&create, "The calendar to create")
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- created, sessionState, state, lang, jerr := g.jmap.CreateCalendar(accountId, create, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
- })
+ create(Calendar, w, r, g, nil, g.jmap.CreateCalendar)
}
func (g *Groupware) DeleteCalendar(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
- l := req.logger.With().Str(accountId, log.SafeString(accountId))
-
- calendarId, err := req.PathParam(UriParamCalendarId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamCalendarId, log.SafeString(calendarId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- deleted, sessionState, state, lang, jerr := g.jmap.DeleteCalendar(accountId, single(calendarId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- for _, e := range deleted {
- desc := e.Description
- if desc != "" {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteCalendar,
- withDetail(e.Description),
- ))
- } else {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteCalendar,
- ))
- }
- }
- return req.noContent(accountId, sessionState, CalendarResponseObjectType, state)
- })
+ delete(Calendar, w, r, g, g.jmap.DeleteCalendar)
}
func (g *Groupware) ModifyCalendar(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
- l := req.logger.With().Str(accountId, log.SafeString(accountId))
- id, err := req.PathParamDoc(UriParamCalendarId, "The unique identifier of the Calendar to modify")
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamCalendarId, log.SafeString(id))
-
- var change jmap.CalendarChange
- err = req.body(&change)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendar(accountId, id, change, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, updated, sessionState, CalendarResponseObjectType, state)
- })
+ modify(Calendar, w, r, g, g.jmap.UpdateCalendar)
}
diff --git a/services/groupware/pkg/groupware/api_changes.go b/services/groupware/pkg/groupware/api_changes.go
index 07e4b88335..40a386ef76 100644
--- a/services/groupware/pkg/groupware/api_changes.go
+++ b/services/groupware/pkg/groupware/api_changes.go
@@ -82,8 +82,8 @@ func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSON
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
- var body jmap.Changes = changes
+ var body jmap.ObjectChanges = changes
- return req.respond(accountId, body, sessionState, "", state)
+ return req.respond(accountId, body, sessionState, "", state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go
index ebceefde6a..381f4d0240 100644
--- a/services/groupware/pkg/groupware/api_contacts.go
+++ b/services/groupware/pkg/groupware/api_contacts.go
@@ -91,7 +91,7 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
}
if contacts, ok := contactsByAccountId[accountId]; ok {
- return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state)
+ return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state, lang)
} else {
return req.notFoundN(accountIds, sessionState, ContactResponseObjectType, state)
}
@@ -99,192 +99,36 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
}
func (g *Groupware) GetContactById(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()
-
- contactId, err := req.PathParam(UriParamContactId)
- if err != nil {
- return req.error(accountId, err)
- }
- l = l.Str(UriParamContactId, log.SafeString(contactId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, single(contactId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- switch len(contacts.List) {
- case 0:
- return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
- case 1:
- return req.respond(accountId, contacts.List[0], sessionState, ContactResponseObjectType, state)
- default:
- logger.Error().Msgf("found %d contacts matching '%s' instead of 1", len(contacts.List), contactId)
- return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
- }
- })
+ get(Contact, w, r, g, g.jmap.GetContactCards)
}
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)
- ctx := req.ctx.WithLogger(logger)
- contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, []string{}, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- var body []jmap.ContactCard = contacts.List
-
- return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
- })
+ getallpaged(Contact, w, r, g,
+ g.jmap.GetContactCards,
+ func(cid string) jmap.ContactCardFilterElement {
+ return jmap.ContactCardFilterCondition{InAddressBook: cid}
+ },
+ []jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyUpdated, IsAscending: true}},
+ curryMapQuery(g.jmap.QueryContactCards),
+ )
}
// 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 := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list contact changes"))
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetContactCardChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- var body jmap.ContactCardChanges = changes
-
- return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
- })
+ changes(Contact, w, r, g, g.jmap.GetContactCardChanges)
}
func (g *Groupware) CreateContact(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()
-
- addressBookId, err := req.PathParam(UriParamAddressBookId)
- if err != nil {
- return req.error(accountId, err)
- }
- l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
-
- var create jmap.ContactCard
- err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- created, sessionState, state, lang, jerr := g.jmap.CreateContactCard(accountId, create, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
- })
+ create(Contact, w, r, g, nil, g.jmap.CreateContactCard)
}
func (g *Groupware) DeleteContact(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().Str(accountId, log.SafeString(accountId))
-
- contactId, err := req.PathParam(UriParamContactId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamContactId, log.SafeString(contactId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- deleted, sessionState, state, lang, jerr := g.jmap.DeleteContactCard(accountId, single(contactId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- for _, e := range deleted {
- desc := e.Description
- if desc != "" {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteContact,
- withDetail(e.Description),
- ))
- } else {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteContact,
- ))
- }
- }
- return req.noContent(accountId, sessionState, ContactResponseObjectType, state)
- })
+ delete(Contact, w, r, g, g.jmap.DeleteContactCard)
}
func (g *Groupware) ModifyContact(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().Str(accountId, log.SafeString(accountId))
- id, err := req.PathParamDoc(UriParamContactId, "The unique identifier of the Contact to modify")
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamContactId, log.SafeString(id))
-
- var change jmap.ContactCardChange
- err = req.body(&change)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- updated, sessionState, state, lang, jerr := g.jmap.UpdateContactCard(accountId, id, change, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, updated, sessionState, ContactResponseObjectType, state)
- })
+ modify(Contact, w, r, g, g.jmap.UpdateContactCard)
}
func mapContactCardSort(s SortCrit) jmap.ContactCardComparator {
diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go
index e8b791473a..60b942d99f 100644
--- a/services/groupware/pkg/groupware/api_emails.go
+++ b/services/groupware/pkg/groupware/api_emails.go
@@ -24,37 +24,8 @@ import (
// Get the changes tp Emails since a certain State.
// @api:tags email,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 req.error(accountId, err)
- }
- l = l.Str(logAccountId, accountId)
-
- maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Uint(QueryParamMaxChanges, maxChanges)
- }
-
- 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(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetEmailChanges(accountId, sinceState, true, g.config.maxBodyValueBytes, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state)
+ changes(Email, w, r, g, func(accountId string, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.EmailChanges, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return g.jmap.GetEmailChanges(accountId, sinceState, true, g.config.maxBodyValueBytes, maxChanges, ctx)
})
}
@@ -67,63 +38,31 @@ func (g *Groupware) GetEmailChanges(w http.ResponseWriter, r *http.Request) {
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
// respectively.
func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) { //NOSONAR
- g.respond(w, r, func(req Request) Response {
- l := req.logger.With()
+ collapseThreads := false
+ fetchBodies := false
+ withThreads := true
+ query(Email, w, r, g, g.defaults.emailLimit,
+ func(req Request, accountId, containerId string, offset int, limit uint, ctx jmap.Context) (jmap.EmailSearchResults, jmap.SessionState, jmap.State, jmap.Language, *Error) {
+ emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, containerId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
+ if jerr != nil {
+ return emails, sessionState, state, lang, req.apiErrorFromJmap(req.observeJmapError(jerr))
+ }
- accountId, err := req.GetAccountIdForMail()
- if err != nil {
- return req.error(accountId, err)
- }
- l = l.Str(logAccountId, accountId)
+ sanitized, err := req.sanitizeEmails(emails.Results)
+ if err != nil {
+ return emails, sessionState, state, lang, err
+ }
- mailboxId, err := req.PathParam(UriParamMailboxId)
- if err != nil {
- return req.error(accountId, err)
- }
-
- offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Int(QueryParamOffset, offset)
- }
-
- limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Uint(QueryParamLimit, limit)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- collapseThreads := false
- fetchBodies := false
- withThreads := true
-
- emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- sanitized, err := req.sanitizeEmails(emails.Results)
- if err != nil {
- return req.error(accountId, err)
- }
-
- safe := jmap.EmailSearchResults{
- Results: sanitized,
- Total: emails.Total,
- Limit: emails.Limit,
- Position: emails.Position,
- CanCalculateChanges: emails.CanCalculateChanges,
- }
-
- return req.respond(accountId, safe, sessionState, EmailResponseObjectType, state)
- })
+ safe := jmap.EmailSearchResults{
+ Results: sanitized,
+ Total: emails.Total,
+ Limit: emails.Limit,
+ Position: emails.Position,
+ CanCalculateChanges: emails.CanCalculateChanges,
+ }
+ return safe, sessionState, state, lang, nil
+ },
+ )
}
func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NOSONAR
@@ -196,7 +135,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
}
if len(ids) == 1 {
- logger := log.From(l.Str("id", log.SafeString(id)))
+ logger := log.From(l.Str(UriParamEmailId, log.SafeString(id)))
ctx := req.ctx.WithLogger(logger)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, ids, true, g.config.maxBodyValueBytes, markAsSeen, true, ctx)
if jerr != nil {
@@ -209,10 +148,10 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
if err != nil {
return req.error(accountId, err)
}
- return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state, lang)
}
} else {
- logger := log.From(l.Array("ids", log.SafeStringArray(ids)))
+ logger := log.From(l.Array(UriParamEmailId, log.SafeStringArray(ids)))
ctx := req.ctx.WithLogger(logger)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, ids, true, g.config.maxBodyValueBytes, markAsSeen, false, ctx)
if jerr != nil {
@@ -225,7 +164,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
if err != nil {
return req.error(accountId, err)
}
- return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state, lang)
}
}
})
@@ -284,7 +223,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
return req.error(accountId, err)
}
var body []jmap.EmailBodyPart = email.Attachments
- return req.respond(accountId, body, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, body, sessionState, EmailResponseObjectType, state, lang)
})
} else {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
@@ -399,7 +338,7 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since
return req.jmapError(accountId, jerr, sessionState, lang)
}
- return req.respond(accountId, changes, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, changes, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -428,7 +367,8 @@ type SnippetWithoutEmailId struct {
type EmailWithSnippetsSearchResults struct {
Results []EmailWithSnippets `json:"results"`
- Total uint `json:"total,omitzero"`
+ Total *uint `json:"total,omitzero"`
+ Position uint `json:"position"`
Limit uint `json:"limit,omitzero"`
QueryState jmap.State `json:"queryState,omitempty"`
}
@@ -610,20 +550,32 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA
return req.error(accountId, err)
}
+ l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
+
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return req.error(accountId, err)
}
- logger = log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)))
- ctx := req.ctx.WithLogger(logger)
if !filter.IsNotEmpty() {
filter = nil
}
- fetchBodies := false
+ calculateTotal := true
+ if b, ok, err := req.parseBoolParam(QueryParamCalculateTotal, true); err != nil {
+ return req.error(accountId, err)
+ } else if ok {
+ calculateTotal = b
+ l = l.Bool(QueryParamCalculateTotal, calculateTotal)
+ }
- resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, offset, limit, fetchBodies, g.config.maxBodyValueBytes, ctx)
+ fetchBodies := false
+ collapseThreads := false
+
+ logger = log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+
+ resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, offset, limit, collapseThreads, calculateTotal, fetchBodies, g.config.maxBodyValueBytes, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
@@ -653,12 +605,18 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA
}
}
+ var total *uint = nil
+ if calculateTotal {
+ total = &results.Total
+ }
+
return req.respond(accountId, EmailWithSnippetsSearchResults{
Results: flattened,
- Total: results.Total,
+ Total: total,
+ Position: results.Position,
Limit: results.Limit,
QueryState: results.QueryState,
- }, sessionState, EmailResponseObjectType, state)
+ }, sessionState, EmailResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, EmailResponseObjectType, state)
}
@@ -720,7 +678,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
QueryState: state,
}
- return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state)
+ return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
} else {
withThreads := true
@@ -759,7 +717,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
QueryState: state,
}
- return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state)
+ return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -822,127 +780,53 @@ func findSentMailboxId(j *jmap.Client, accountId string, req Request, ctx jmap.C
}
func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- logger := req.logger
-
- accountId, gwerr := req.GetAccountIdForMail()
- if gwerr != nil {
- return req.error(accountId, gwerr)
- }
- logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- ctx := req.ctx.WithLogger(logger)
-
- var body jmap.EmailCreate
- err := req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
-
- if len(body.MailboxIds) < 1 {
- mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, req, ctx)
- if mailboxId != "" {
- body.MailboxIds[mailboxId] = true
- } else {
- return resp
+ create(Email, w, r, g,
+ func(r Request, accountId string, body *jmap.EmailChange, ctx jmap.Context) (bool, Response) {
+ if len(body.MailboxIds) < 1 {
+ mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, r, ctx)
+ if mailboxId != "" {
+ body.MailboxIds[mailboxId] = true
+ } else {
+ return false, resp
+ }
}
- }
-
- created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, "", ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, created, sessionState, EmailResponseObjectType, state)
- })
+ return true, Response{}
+ },
+ func(accountId string, body jmap.EmailChange, ctx jmap.Context) (*jmap.Email, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return g.jmap.CreateEmail(accountId, body, "", ctx)
+ },
+ )
}
func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- logger := req.logger
-
- accountId, gwerr := req.GetAccountIdForMail()
- if gwerr != nil {
- return req.error(accountId, gwerr)
- }
-
- replaceId, err := req.PathParam(UriParamEmailId)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- ctx := req.ctx.WithLogger(logger)
-
- var body jmap.EmailCreate
- err = req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
-
- if len(body.MailboxIds) < 1 {
- mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, req, ctx)
- if mailboxId != "" {
- body.MailboxIds[mailboxId] = true
- } else {
- return resp
+ replaceId := ""
+ create(Email, w, r, g,
+ func(r Request, accountId string, body *jmap.EmailChange, ctx jmap.Context) (bool, Response) {
+ if len(body.MailboxIds) < 1 {
+ mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, r, ctx)
+ if mailboxId != "" {
+ body.MailboxIds[mailboxId] = true
+ } else {
+ return false, resp
+ }
+ }
+ var err *Error
+ replaceId, err = r.PathParam(UriParamEmailId)
+ if err != nil {
+ return false, r.error(accountId, err)
}
- }
- created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, replaceId, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, created, sessionState, EmailResponseObjectType, state)
- })
+ return true, Response{}
+ },
+ func(accountId string, body jmap.EmailChange, ctx jmap.Context) (*jmap.Email, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ ctx = ctx.WithLogger(log.From(ctx.Logger.With().Str("replaceId", replaceId)))
+ return g.jmap.CreateEmail(accountId, body, replaceId, ctx)
+ },
+ )
}
func (g *Groupware) UpdateEmail(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 req.error(accountId, gwerr)
- }
- l.Str(logAccountId, accountId)
-
- emailId, err := req.PathParam(UriParamEmailId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamEmailId, log.SafeString(emailId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- var body map[string]any
- err = req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
-
- updates := map[string]jmap.EmailUpdate{
- emailId: body,
- }
-
- result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, updates, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- if result == nil {
- return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR
- "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR
- }
- updatedEmail, ok := result[emailId]
- if !ok {
- return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR
- "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR
- }
-
- return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
- })
+ modify(Email, w, r, g, g.jmap.UpdateEmail)
}
type emailKeywordUpdates struct {
@@ -986,14 +870,14 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
return req.noop(accountId)
}
- patch := jmap.EmailUpdate{}
+ patch := jmap.PatchObject{}
for _, keyword := range body.Add {
patch["keywords/"+keyword] = true //NOSONAR
}
for _, keyword := range body.Remove {
patch["keywords/"+keyword] = nil //NOSONAR
}
- patches := map[string]jmap.EmailUpdate{
+ patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1003,16 +887,16 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
}
if result == nil {
- 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")))
+ return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR
+ "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR
}
updatedEmail, ok := result[emailId]
if !ok {
- 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 req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR
+ "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR
}
- return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1048,11 +932,11 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { /
return req.noop(accountId)
}
- patch := jmap.EmailUpdate{}
+ patch := jmap.PatchObject{}
for _, keyword := range body {
patch["keywords/"+keyword] = true
}
- patches := map[string]jmap.EmailUpdate{
+ patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1074,7 +958,7 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { /
if updatedEmail == nil {
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
} else {
- return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -1111,11 +995,11 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
return req.noop(accountId)
}
- patch := jmap.EmailUpdate{}
+ patch := jmap.PatchObject{}
for _, keyword := range body {
patch["keywords/"+keyword] = nil
}
- patches := map[string]jmap.EmailUpdate{
+ patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1137,52 +1021,14 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
if updatedEmail == nil {
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
} else {
- return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
}
})
}
// Delete an email by its unique identifier.
func (g *Groupware) DeleteEmail(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 req.error(accountId, gwerr)
- }
- l.Str(logAccountId, log.SafeString(accountId))
-
- emailId, err := req.PathParam(UriParamEmailId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamEmailId, log.SafeString(emailId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, single(emailId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- for _, e := range resp {
- desc := e.Description
- if desc != "" {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteEmail,
- withDetail(e.Description),
- ))
- } else {
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteEmail,
- ))
- }
- }
- return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
- })
+ delete(Email, w, r, g, g.jmap.DeleteEmails)
}
// Delete a set of emails by their unique identifiers.
@@ -1190,44 +1036,7 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
// The identifiers of the emails to delete are specified as part of the request
// body, as an array of strings.
func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
- /// @api body
- g.respond(w, r, func(req Request) Response {
- l := req.logger.With()
-
- accountId, gwerr := req.GetAccountIdForMail()
- if gwerr != nil {
- return req.error(accountId, gwerr)
- }
- l.Str(logAccountId, accountId)
-
- var emailIds []string
- err := req.body(&emailIds)
- if err != nil {
- return req.error(accountId, err)
- }
-
- l.Array("emailIds", log.SafeStringArray(emailIds))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, emailIds, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- if len(resp) > 0 {
- meta := make(map[string]any, len(resp))
- for emailId, e := range resp {
- meta[emailId] = e.Description
- }
- return req.error(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteEmail,
- withMeta(meta),
- ))
- }
- return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
- })
+ deleteMany(Email, w, r, g, g.jmap.DeleteEmails)
}
func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONAR
@@ -1285,7 +1094,7 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONA
return req.jmapError(accountId, jerr, sessionState, lang)
}
- return req.respond(accountId, resp, sessionState, EmailResponseObjectType, state)
+ return req.respond(accountId, resp, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1456,7 +1265,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { //N
return req.respond(accountId, AboutEmailResponse{
Email: sanitized,
RequestId: reqId,
- }, sessionState, EmailResponseObjectType, state)
+ }, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1733,7 +1542,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
Total: total,
Limit: limit,
Offset: offset,
- }, sessionState, EmailResponseObjectType, state)
+ }, sessionState, EmailResponseObjectType, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go
index 4c2195687c..9e4931812d 100644
--- a/services/groupware/pkg/groupware/api_events.go
+++ b/services/groupware/pkg/groupware/api_events.go
@@ -43,7 +43,7 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
filter := jmap.CalendarEventFilterCondition{
InCalendar: calendarId,
}
- sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyUpdated, IsAscending: false}}
+ sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: false}}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
@@ -53,139 +53,55 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
}
if events, ok := eventsByAccountId[accountId]; ok {
- return req.respond(accountId, events, sessionState, EventResponseObjectType, state)
+ return req.respond(accountId, events, sessionState, EventResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, EventResponseObjectType, state)
}
})
}
-// Get changes to Contacts since a given State
+//func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
+
+func curryMapQuery[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any](
+ f func(accountIds []string, filter FILTER, sortBy []COMP, position int, limit uint, calculateTotal bool, ctx jmap.Context) (map[string]SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ m, sessionState, state, lang, err := f(single(accountId), filter, sortBy, offset, limit, true, ctx)
+ return m[accountId], sessionState, state, lang, err
+ }
+}
+
+func (g *Groupware) GetAllEvents(w http.ResponseWriter, r *http.Request) {
+ getallpaged(Event, w, r, g,
+ g.jmap.GetCalendarEvents,
+ func(cid string) jmap.CalendarEventFilterElement {
+ return jmap.CalendarEventFilterCondition{InCalendar: cid}
+ },
+ []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}},
+ curryMapQuery(g.jmap.QueryCalendarEvents),
+ )
+}
+
+func (g *Groupware) GetEventById(w http.ResponseWriter, r *http.Request) {
+ get(Event, w, r, g, g.jmap.GetCalendarEvents)
+}
+
+// Get changes to Calendar Events since a given State
// @api:tags event,changes
func (g *Groupware) GetEventChanges(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- 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 := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list event changes"))
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetCalendarEventChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- var body jmap.CalendarEventChanges = changes
-
- return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
- })
+ changes(Event, w, r, g, g.jmap.GetCalendarEventChanges)
}
-func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
-
- l := req.logger.With()
-
- var create jmap.CalendarEvent
- err := req.body(&create)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- created, sessionState, state, lang, jerr := g.jmap.CreateCalendarEvent(accountId, create, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, created, sessionState, EventResponseObjectType, state)
- })
+func (g *Groupware) CreateEvent(w http.ResponseWriter, r *http.Request) {
+ create(Event, w, r, g, nil, g.jmap.CreateCalendarEvent)
}
-func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
- l := req.logger.With().Str(accountId, log.SafeString(accountId))
-
- eventId, err := req.PathParam(UriParamEventId)
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamEventId, log.SafeString(eventId))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- deleted, sessionState, state, lang, jerr := g.jmap.DeleteCalendarEvent(accountId, single(eventId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- for _, e := range deleted {
- desc := e.Description
- if desc != "" {
- return req.errorS(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteContact,
- withDetail(e.Description),
- ), sessionState)
- } else {
- return req.errorS(accountId, apiError(
- req.errorId(),
- ErrorFailedToDeleteContact,
- ), sessionState)
- }
- }
- return req.noContent(accountId, sessionState, EventResponseObjectType, state)
- })
+func (g *Groupware) DeleteEvent(w http.ResponseWriter, r *http.Request) {
+ delete(Event, w, r, g, g.jmap.DeleteCalendarEvent)
}
-func (g *Groupware) ModifyCalendarEvent(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needCalendarWithAccount()
- if !ok {
- return resp
- }
- l := req.logger.With().Str(accountId, log.SafeString(accountId))
- id, err := req.PathParamDoc(UriParamEventId, "The unique identifier of the Calendar Event to modify")
- if err != nil {
- return req.error(accountId, err)
- }
- l.Str(UriParamEventId, log.SafeString(id))
-
- var change jmap.CalendarEventChange
- err = req.body(&change)
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendarEvent(accountId, id, change, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, updated, sessionState, EventResponseObjectType, state)
- })
+func (g *Groupware) ModifyEvent(w http.ResponseWriter, r *http.Request) {
+ modify(Event, w, r, g, g.jmap.UpdateCalendarEvent)
}
// Parse a blob that contains an iCal file and return it as JSCalendar.
@@ -211,6 +127,6 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
- return req.respond(accountId, resp, sessionState, EventResponseObjectType, state)
+ return req.respond(accountId, resp, sessionState, EventResponseObjectType, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/api_identity.go b/services/groupware/pkg/groupware/api_identity.go
index bdbff5fa39..15d464d170 100644
--- a/services/groupware/pkg/groupware/api_identity.go
+++ b/services/groupware/pkg/groupware/api_identity.go
@@ -1,170 +1,33 @@
package groupware
import (
- "fmt"
"net/http"
- "strings"
-
- "github.com/opencloud-eu/opencloud/pkg/jmap"
- "github.com/opencloud-eu/opencloud/pkg/log"
)
// Get the list of identities that are associated with an account.
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 req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId))
- ctx := req.ctx.WithLogger(logger)
- res, sessionState, state, lang, jerr := g.jmap.GetAllIdentities(accountId, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, res, sessionState, IdentityResponseObjectType, state)
- })
+ getall(Identity, w, r, g, g.jmap.GetIdentities)
}
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 req.error(accountId, err)
- }
- id, err := req.PathParam(UriParamIdentityId)
- if err != nil {
- return req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
- ctx := req.ctx.WithLogger(logger)
- res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, single(id), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- if len(res) < 1 {
- return req.notFound(accountId, sessionState, IdentityResponseObjectType, state)
- }
- var body jmap.Identity = res[0]
- return req.respond(accountId, body, sessionState, IdentityResponseObjectType, state)
- })
+ get(Identity, w, r, g, g.jmap.GetIdentities)
}
-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 req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId))
- ctx := req.ctx.WithLogger(logger)
-
- var identity jmap.IdentityChange
- err = req.body(&identity)
- if err != nil {
- return req.error(accountId, err)
- }
-
- created, sessionState, state, lang, jerr := g.jmap.CreateIdentity(accountId, identity, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, created, sessionState, IdentityResponseObjectType, state)
- })
+func (g *Groupware) CreateIdentity(w http.ResponseWriter, r *http.Request) {
+ create(Identity, w, r, g, nil, g.jmap.CreateIdentity)
}
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 req.error(accountId, err)
- }
- id, err := req.PathParamDoc(UriParamIdentityId, "The unique identifier of the Identity to modify")
- if err != nil {
- return req.error(accountId, err)
- }
-
- logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(UriParamIdentityId, log.SafeString(id)))
- ctx := req.ctx.WithLogger(logger)
-
- var identity jmap.IdentityChange
- err = req.body(&identity)
- if err != nil {
- return req.error(accountId, err)
- }
-
- updated, sessionState, state, lang, jerr := g.jmap.UpdateIdentity(accountId, id, identity, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, updated, sessionState, IdentityResponseObjectType, state)
- })
+ modify(Identity, w, r, g, g.jmap.UpdateIdentity)
}
// Delete an identity.
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 req.error(accountId, err)
- }
-
- id, err := req.PathParam(UriParamIdentityId)
- if err != nil {
- 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"))
- }
-
- logger := log.From(req.logger.With().Str(logAccountId, accountId).Array(UriParamIdentityId, log.SafeStringArray(ids)))
- ctx := req.ctx.WithLogger(logger)
-
- notDeleted, sessionState, state, lang, jerr := g.jmap.DeleteIdentity(accountId, ids, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- if len(notDeleted) == 0 {
- return req.noContent(accountId, sessionState, IdentityResponseObjectType, state)
- } else {
- logger.Error().Msgf("failed to delete %d identities", len(notDeleted))
- return req.errorS(accountId, req.apiError(&ErrorFailedToDeleteSomeIdentities), sessionState)
- }
- })
+ delete(Identity, w, r, g, g.jmap.DeleteIdentity)
}
// Get changes to Identities since a given State
// @api:tags identity,changes
func (g *Groupware) GetIdentityChanges(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- accountId, err := req.GetAccountIdForMail()
- if err != nil {
- return req.error(accountId, err)
- }
-
- l := req.logger.With().Str(logAccountId, accountId)
-
- 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 := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list identity changes"))
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
- changes, sessionState, state, lang, jerr := g.jmap.GetIdentityChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- var body jmap.IdentityChanges = changes
-
- return req.respond(accountId, body, sessionState, IdentityResponseObjectType, state)
- })
+ changes(Identity, w, r, g, g.jmap.GetIdentityChanges)
}
diff --git a/services/groupware/pkg/groupware/api_index.go b/services/groupware/pkg/groupware/api_index.go
index b17b0718b9..13a37998eb 100644
--- a/services/groupware/pkg/groupware/api_index.go
+++ b/services/groupware/pkg/groupware/api_index.go
@@ -160,7 +160,7 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
Accounts: buildIndexAccounts(req.session, boot),
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
}
- return req.respondN(accountIds, body, sessionState, IndexResponseObjectType, state)
+ return req.respondN(accountIds, body, sessionState, IndexResponseObjectType, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go
index 13801cdf53..25fe6ce866 100644
--- a/services/groupware/pkg/groupware/api_mailbox.go
+++ b/services/groupware/pkg/groupware/api_mailbox.go
@@ -18,29 +18,12 @@ import (
//
// This is the primary mechanism for organising Emails within an account.
// It is analogous to a folder or a label in other systems.
-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 req.error(accountId, err)
- }
+func (g *Groupware) GetMailboxById(w http.ResponseWriter, r *http.Request) {
+ get(Mailbox, w, r, g, g.jmap.GetMailbox)
+}
- mailboxId, err := req.PathParam(UriParamMailboxId)
- if err != nil {
- return req.error(accountId, err)
- }
-
- mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, single(mailboxId), req.ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- if len(mailboxes.List) == 1 {
- return req.respond(accountId, mailboxes.List[0], sessionState, MailboxResponseObjectType, state)
- } else {
- return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
- }
- })
+func (g *Groupware) ModifyMailbox(w http.ResponseWriter, r *http.Request) {
+ modify(Mailbox, w, r, g, g.jmap.UpdateMailbox)
}
// Get the list of all the mailboxes of an account, potentially filtering on the
@@ -92,7 +75,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
- return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state)
+ return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
}
@@ -102,7 +85,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS
return req.jmapError(accountId, err, sessionState, lang)
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
- return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state)
+ return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
}
@@ -140,13 +123,13 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
if err != nil {
return req.jmapErrorN(accountIds, err, sessionState, lang)
}
- return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
+ return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
} else {
mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes(accountIds, ctx)
if err != nil {
return req.jmapErrorN(accountIds, err, sessionState, lang)
}
- return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
+ return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
}
})
}
@@ -175,58 +158,14 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt
if jerr != nil {
return req.jmapErrorN(accountIds, jerr, sessionState, lang)
}
- return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
+ return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
})
}
// Get the changes tp Mailboxes 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 req.error(accountId, err)
- }
- l = l.Str(logAccountId, accountId)
-
- maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
- if err != nil {
- return req.error(accountId, err)
- }
- if ok {
- l = l.Uint(QueryParamMaxChanges, maxChanges)
- }
-
- sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list mailbox changes"))
- if sinceState != "" {
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- // 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",
- )))
- }
-
- changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, sinceState, maxChanges, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state)
- })
+ changes(Mailbox, w, r, g, g.jmap.GetMailboxChanges)
}
// Get the changes that occured in all the mailboxes of all accounts.
@@ -266,7 +205,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
return req.jmapErrorN(allAccountIds, jerr, sessionState, lang)
}
- return req.respondN(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state)
+ return req.respondN(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state, lang)
})
}
@@ -285,67 +224,12 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
return req.jmapErrorN(allAccountIds, jerr, sessionState, lang)
}
- return req.respondN(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state)
- })
-}
-
-func (g *Groupware) UpdateMailbox(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 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 req.error(accountId, err)
- }
- l = l.Str(UriParamMailboxId, log.SafeString(mailboxId))
-
- var body jmap.MailboxChange
- err = req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- updated, sessionState, state, lang, jerr := g.jmap.UpdateMailbox(accountId, mailboxId, body, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, updated, sessionState, MailboxResponseObjectType, state)
+ return req.respondN(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state, lang)
})
}
func (g *Groupware) CreateMailbox(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 req.error(accountId, err)
- }
- l = l.Str(logAccountId, accountId)
-
- var body jmap.MailboxChange
- err = req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- created, sessionState, state, lang, jerr := g.jmap.CreateMailbox(accountId, body, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, created, sessionState, MailboxResponseObjectType, state)
- })
+ create(Mailbox, w, r, g, nil, g.jmap.CreateMailbox)
}
// Delete Mailboxes by their unique identifiers.
@@ -354,34 +238,7 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
//
// @api:example deletedmailboxes
func (g *Groupware) DeleteMailbox(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 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 req.error(accountId, err)
- }
- l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
-
- if len(mailboxIds) < 1 {
- return req.noop(accountId) // no mailbox identifiers were mentioned in the request
- }
-
- logger := log.From(l)
- ctx := req.ctx.WithLogger(logger)
-
- deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, mailboxIds, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, deleted, sessionState, MailboxResponseObjectType, state)
- })
+ delete(Mailbox, w, r, g, g.jmap.DeleteMailboxes)
}
var mailboxRoleSortOrderScore = map[string]int{
diff --git a/services/groupware/pkg/groupware/api_objects.go b/services/groupware/pkg/groupware/api_objects.go
index 7af25a0f39..87a7692f74 100644
--- a/services/groupware/pkg/groupware/api_objects.go
+++ b/services/groupware/pkg/groupware/api_objects.go
@@ -130,6 +130,6 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON
}
var body jmap.Objects = objs
- return req.respond(accountId, body, sessionState, "", state)
+ return req.respond(accountId, body, sessionState, "", state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/api_quota.go b/services/groupware/pkg/groupware/api_quota.go
index 5772ba1888..486c09d147 100644
--- a/services/groupware/pkg/groupware/api_quota.go
+++ b/services/groupware/pkg/groupware/api_quota.go
@@ -13,22 +13,8 @@ import (
//
// Note that there may be multiple Quota objects for different resource types.
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 req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId))
- ctx := req.ctx.WithLogger(logger)
-
- res, sessionState, state, lang, jerr := g.jmap.GetQuotas(single(accountId), ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- for _, quotas := range res {
- return req.respond(accountId, quotas, sessionState, QuotaResponseObjectType, state)
- }
- return req.notFound(accountId, sessionState, QuotaResponseObjectType, state)
+ getFromMap(Quota, w, r, g, func(accountIds, _ []string, ctx jmap.Context) (map[string]jmap.QuotaGetResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return g.jmap.GetQuotas(accountIds, ctx)
})
}
@@ -62,42 +48,15 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
Quotas: accountQuotas.List,
}
}
- return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state)
+ return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state, lang)
})
}
-// currently unsupported in Stalwart:
-/*
// Get changes to Quotas since a given State
+//
+// Currently unsupported in Stalwart.
// @api:tags contact,changes
+// @api:ignore
func (g *Groupware) GetQuotaChanges(w http.ResponseWriter, r *http.Request) {
- g.respond(w, r, func(req Request) Response {
- accountId, err := req.GetAccountIdForQuota()
- if err != nil {
- return req.error(accountId, err)
- }
-
- l := req.logger.With().Str(logAccountId, accountId)
-
- 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 := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list quota changes"))
- l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
-
- logger := log.From(l)
- changes, sessionState, state, lang, jerr := g.jmap.GetQuotaChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- var body jmap.QuotaChanges = changes
-
- return req.respond(accountId, body, sessionState, QuotaResponseObjectType, state)
- })
+ changes(Quota, w, r, g, g.jmap.GetQuotaChanges)
}
-*/
diff --git a/services/groupware/pkg/groupware/api_tasklists.go b/services/groupware/pkg/groupware/api_tasklists.go
index b2d110c681..ee4ad494d4 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 req.respond(accountId, body, req.session.State, TaskListResponseObjectType, TaskListsState)
+ return req.respond(accountId, body, req.session.State, TaskListResponseObjectType, TaskListsState, jmap.NoLanguage)
})
}
@@ -36,7 +36,7 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
// TODO replace with proper implementation
for _, tasklist := range AllTaskLists {
if tasklist.Id == tasklistId {
- return req.respond(accountId, tasklist, req.session.State, TaskListResponseObjectType, TaskListsState)
+ return req.respond(accountId, tasklist, req.session.State, TaskListResponseObjectType, TaskListsState, jmap.NoLanguage)
}
}
return req.etaggedNotFound(accountId, req.session.State, TaskListResponseObjectType, TaskListsState)
@@ -61,6 +61,6 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
if !ok {
return req.notFound(accountId, req.session.State, TaskResponseObjectType, TaskState)
}
- return req.respond(accountId, tasks, req.session.State, TaskResponseObjectType, TaskState)
+ return req.respond(accountId, tasks, req.session.State, TaskResponseObjectType, TaskState, jmap.NoLanguage)
})
}
diff --git a/services/groupware/pkg/groupware/api_vacation.go b/services/groupware/pkg/groupware/api_vacation.go
index 1cd99307d0..6226610089 100644
--- a/services/groupware/pkg/groupware/api_vacation.go
+++ b/services/groupware/pkg/groupware/api_vacation.go
@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
- "github.com/opencloud-eu/opencloud/pkg/log"
)
// Get vacation notice information.
@@ -14,19 +13,8 @@ import (
//
// The VacationResponse object represents the state of vacation-response-related settings for an account.
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 req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId))
- ctx := req.ctx.WithLogger(logger)
-
- res, sessionState, state, lang, jerr := g.jmap.GetVacationResponse(accountId, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
- return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state)
+ get(VacationResponse, w, r, g, func(accountId string, ids []string, ctx jmap.Context) (jmap.VacationResponseGetResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return g.jmap.GetVacationResponse(accountId, ctx)
})
}
@@ -35,25 +23,7 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
// A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original
// sender that their message may not be read for some time.
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 req.error(accountId, err)
- }
- logger := log.From(req.logger.With().Str(logAccountId, accountId))
- ctx := req.ctx.WithLogger(logger)
-
- var body jmap.VacationResponseChange
- err = req.body(&body)
- if err != nil {
- return req.error(accountId, err)
- }
-
- res, sessionState, state, lang, jerr := g.jmap.SetVacationResponse(accountId, body, ctx)
- if jerr != nil {
- return req.jmapError(accountId, jerr, sessionState, lang)
- }
-
- return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state)
+ modify(VacationResponse, w, r, g, func(accountId string, id string, change jmap.VacationResponseChange, ctx jmap.Context) (jmap.VacationResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
+ return g.jmap.SetVacationResponse(accountId, change, ctx)
})
}
diff --git a/services/groupware/pkg/groupware/error.go b/services/groupware/pkg/groupware/error.go
index 9a45694164..c4fe9043ca 100644
--- a/services/groupware/pkg/groupware/error.go
+++ b/services/groupware/pkg/groupware/error.go
@@ -191,18 +191,27 @@ const (
ErrorCodeAccountNotFound = "ACCNFD"
ErrorCodeAccountNotSupportedByMethod = "ACCNSM"
ErrorCodeAccountReadOnly = "ACCRDO"
+ ErrorCodeMissingBlobSessionCapability = "MSCBLO"
+ ErrorCodeMissingBlobAccountCapability = "MACBLO"
+ ErrorCodeMissingMailsSessionCapability = "MSCMAI"
+ ErrorCodeMissingMailsAccountCapability = "MACMAI"
ErrorCodeMissingCalendarsSessionCapability = "MSCCAL"
ErrorCodeMissingCalendarsAccountCapability = "MACCAL"
ErrorCodeMissingContactsSessionCapability = "MSCCON"
ErrorCodeMissingContactsAccountCapability = "MACCON"
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
ErrorCodeMissingTasksAccountCapability = "MACTSK"
+ ErrorCodeMissingQuotaSessionCapability = "MSCMAI"
+ ErrorCodeMissingQuotaAccountCapability = "MACMAI"
ErrorCodeFailedToDeleteEmail = "DELEML"
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
ErrorCodeFailedToSanitizeEmail = "FSANEM"
ErrorCodeFailedToDeleteAddressBook = "DELABK"
- ErrorCodeFailedToDeleteCalendar = "DELCAL"
+ ErrorCodeFailedToDeleteMailbox = "DELMBX"
ErrorCodeFailedToDeleteContact = "DELCNT"
+ ErrorCodeFailedToDeleteCalendar = "DELCAL"
+ ErrorCodeFailedToDeleteEvent = "DELEVT"
+ ErrorCodeFailedToDeleteIdentity = "DELIDN"
ErrorCodeNoMailboxWithDraftRole = "NMBXDR"
ErrorCodeNoMailboxWithSentRole = "NMBXSE"
ErrorCodeInvalidSortSpecification = "INVSSP"
@@ -392,6 +401,30 @@ var (
Title: "The referenced Account is read-only",
Detail: "The Account that was referenced in the request only supports read-only operations.",
}
+ ErrorMissingBlobSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingBlobAccountCapability,
+ Title: "Session is missing the blob session capability",
+ Detail: "The JMAP Session of the user does not have the required capability for blobs.",
+ }
+ ErrorMissingBlobAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingBlobSessionCapability,
+ Title: "Account is missing the blob capability",
+ Detail: "The JMAP Account of the user does not have the required capability for blobs.",
+ }
+ ErrorMissingMailsSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingMailsAccountCapability,
+ Title: "Session is missing the mails session capability",
+ Detail: "The JMAP Session of the user does not have the required capability for mails.",
+ }
+ ErrorMissingMailsAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingMailsSessionCapability,
+ Title: "Account is missing the mails capability",
+ Detail: "The JMAP Account of the user does not have the required capability for mails.",
+ }
ErrorMissingCalendarsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingCalendarsAccountCapability,
@@ -428,6 +461,18 @@ var (
Title: "Account is missing the tasks capability",
Detail: "The JMAP Account of the user does not have the required capability for tasks",
}
+ ErrorMissingQuotaSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingQuotaSessionCapability,
+ Title: "Session is missing the quota session capability",
+ Detail: "The JMAP Session of the user does not have the required capability for quotas.",
+ }
+ ErrorMissingQuotaAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingQuotaAccountCapability,
+ Title: "Account is missing the quota capability",
+ Detail: "The JMAP Account of the user does not have the required capability for quotas.",
+ }
ErrorFailedToDeleteEmail = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteEmail,
@@ -452,11 +497,17 @@ var (
Title: "Failed to delete address books",
Detail: "One or more address books could not be deleted.",
}
- ErrorFailedToDeleteContact = GroupwareError{
+ ErrorFailedToDeleteMailbox = GroupwareError{
Status: http.StatusInternalServerError,
- Code: ErrorCodeFailedToDeleteContact,
- Title: "Failed to delete contacts",
- Detail: "One or more contacts could not be deleted.",
+ Code: ErrorCodeFailedToDeleteMailbox,
+ Title: "Failed to delete mailboxes",
+ Detail: "One or more mailboxes could not be deleted.",
+ }
+ ErrorFailedToDeleteEvent = GroupwareError{
+ Status: http.StatusInternalServerError,
+ Code: ErrorCodeFailedToDeleteEvent,
+ Title: "Failed to delete events",
+ Detail: "One or more events could not be deleted.",
}
ErrorFailedToDeleteCalendar = GroupwareError{
Status: http.StatusInternalServerError,
@@ -464,6 +515,18 @@ var (
Title: "Failed to delete calendar",
Detail: "One or more calendars could not be deleted.",
}
+ ErrorFailedToDeleteContact = GroupwareError{
+ Status: http.StatusInternalServerError,
+ Code: ErrorCodeFailedToDeleteContact,
+ Title: "Failed to delete contacts",
+ Detail: "One or more contacts could not be deleted.",
+ }
+ ErrorFailedToDeleteIdentity = GroupwareError{
+ Status: http.StatusInternalServerError,
+ Code: ErrorCodeFailedToDeleteIdentity,
+ Title: "Failed to delete identities",
+ Detail: "One or more identities could not be deleted.",
+ }
ErrorNoMailboxWithDraftRole = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeNoMailboxWithDraftRole,
diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go
index 703a35fd8b..5707d16642 100644
--- a/services/groupware/pkg/groupware/framework.go
+++ b/services/groupware/pkg/groupware/framework.go
@@ -43,7 +43,6 @@ const (
logErrorSourceHeader = "source-header"
logErrorSourceParameter = "source-parameter"
logErrorSourcePointer = "source-pointer"
- logIdentityId = "identity-id"
logEmailId = "email-id"
logJobDescription = "job"
logJobId = "job-id"
@@ -108,7 +107,7 @@ type Groupware struct {
jmap *jmap.Client
userProvider userProvider
// SSE events that need to be pushed to clients.
- eventChannel chan Event
+ eventChannel chan SSEvent
// Background jobs that need to be executed.
jobsChannel chan Job
// A threadsafe counter to generate the job IDs.
@@ -132,8 +131,8 @@ func (e GroupwareInitializationError) Unwrap() error {
return e.Err
}
-// SSE Event.
-type Event struct {
+// Ssrver Sent Event.
+type SSEvent struct {
// The type of event, will be sent as the "type" attribute.
Type string
// The ID of the stream to push the event to, typically the username.
@@ -314,7 +313,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
jmapClient.AddSessionEventListener(sessionCache)
// A channel to process SSE Events with a single worker.
- eventChannel := make(chan Event, eventChannelSize)
+ eventChannel := make(chan SSEvent, eventChannelSize)
{
eventBufferSizeMetric, err := prometheus.NewConstMetric(m.EventBufferSizeDesc, prometheus.GaugeValue, float64(eventChannelSize))
if err != nil {
@@ -465,7 +464,7 @@ func (g *Groupware) listenForEvents() {
func (g *Groupware) push(user user, typ string, body any) {
g.metrics.SSEEventsCounter.WithLabelValues(typ).Inc()
- g.eventChannel <- Event{Type: typ, Stream: user.GetUsername(), Body: body}
+ g.eventChannel <- SSEvent{Type: typ, Stream: user.GetUsername(), Body: body}
}
func (g *Groupware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
diff --git a/services/groupware/pkg/groupware/objtypes.go b/services/groupware/pkg/groupware/objtypes.go
new file mode 100644
index 0000000000..010e880a4f
--- /dev/null
+++ b/services/groupware/pkg/groupware/objtypes.go
@@ -0,0 +1,106 @@
+package groupware
+
+import (
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
+)
+
+type ObjectType[T jmap.Foo, CH jmap.Change, CHS jmap.Changes[T]] struct {
+ name string
+ responseType ResponseObjectType
+ uriParamName string
+ containerUriParamName string
+ accountFunc func(r *Request) (bool, string, Response)
+ failedToDeleteError GroupwareError
+}
+
+var (
+ Blob = ObjectType[jmap.Blob, jmap.BlobChange, jmap.BlobChanges]{
+ name: "blob",
+ responseType: BlobResponseObjectType,
+ uriParamName: UriParamBlobId,
+ containerUriParamName: "",
+ accountFunc: (*Request).needBloblWithAccount,
+ failedToDeleteError: ErrorServerUnavailable,
+ }
+
+ AddressBook = ObjectType[jmap.AddressBook, jmap.AddressBookChange, jmap.AddressBookChanges]{
+ name: "address book",
+ responseType: AddressBookResponseObjectType,
+ uriParamName: UriParamAddressBookId,
+ containerUriParamName: "",
+ accountFunc: (*Request).needContactWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteAddressBook,
+ }
+
+ Calendar = ObjectType[jmap.Calendar, jmap.CalendarChange, jmap.CalendarChanges]{
+ name: "calendar",
+ responseType: CalendarResponseObjectType,
+ uriParamName: UriParamCalendarId,
+ containerUriParamName: "",
+ accountFunc: (*Request).needCalendarWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteCalendar,
+ }
+
+ Contact = ObjectType[jmap.ContactCard, jmap.ContactCardChange, jmap.ContactCardChanges]{
+ name: "contact",
+ responseType: ContactResponseObjectType,
+ uriParamName: UriParamContactId,
+ containerUriParamName: UriParamCalendarId,
+ accountFunc: (*Request).needCalendarWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteContact,
+ }
+
+ Email = ObjectType[jmap.Email, jmap.EmailChange, jmap.EmailChanges]{
+ name: "email",
+ responseType: EmailResponseObjectType,
+ uriParamName: UriParamEmailId,
+ containerUriParamName: UriParamMailboxId,
+ accountFunc: (*Request).needMailWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteEmail,
+ }
+
+ Event = ObjectType[jmap.CalendarEvent, jmap.CalendarEventChange, jmap.CalendarEventChanges]{
+ name: "event",
+ responseType: EventResponseObjectType,
+ uriParamName: UriParamEventId,
+ containerUriParamName: UriParamCalendarId,
+ accountFunc: (*Request).needCalendarWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteEvent,
+ }
+
+ Identity = ObjectType[jmap.Identity, jmap.IdentityChange, jmap.IdentityChanges]{
+ name: "identity",
+ responseType: IdentityResponseObjectType,
+ uriParamName: UriParamIdentityId,
+ containerUriParamName: "",
+ accountFunc: (*Request).needMailWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteIdentity,
+ }
+
+ Mailbox = ObjectType[jmap.Mailbox, jmap.MailboxChange, jmap.MailboxChanges]{
+ name: "mailbox",
+ responseType: MailboxResponseObjectType,
+ uriParamName: UriParamMailboxId,
+ containerUriParamName: "",
+ accountFunc: (*Request).needMailWithAccount,
+ failedToDeleteError: ErrorFailedToDeleteMailbox,
+ }
+
+ Quota = ObjectType[jmap.Quota, jmap.QuotaChange, jmap.QuotaChanges]{
+ name: "quota",
+ responseType: QuotaResponseObjectType,
+ uriParamName: "",
+ containerUriParamName: "",
+ accountFunc: (*Request).needQuotaWithAccount,
+ failedToDeleteError: ErrorServerUnavailable,
+ }
+
+ VacationResponse = ObjectType[jmap.VacationResponse, jmap.VacationResponseChange, jmap.VacationResponseChanges]{
+ name: "vacation response",
+ responseType: VacationResponseResponseObjectType,
+ uriParamName: "",
+ containerUriParamName: "",
+ accountFunc: (*Request).needMailWithAccount,
+ failedToDeleteError: ErrorServerUnavailable,
+ }
+)
diff --git a/services/groupware/pkg/groupware/request.go b/services/groupware/pkg/groupware/request.go
index 9fd18ae55f..9357c7fe49 100644
--- a/services/groupware/pkg/groupware/request.go
+++ b/services/groupware/pkg/groupware/request.go
@@ -227,6 +227,16 @@ func (r *Request) parameterErrorResponse(accountIds []string, param string, deta
return r.errorN(accountIds, r.parameterError(param, detail))
}
+func (r *Request) unsupportedParams(accountIds []string, params ...string) (bool, Response) {
+ q := r.r.URL.Query()
+ for _, p := range params {
+ if q.Has(p) {
+ return true, r.parameterErrorResponse(accountIds, p, "Unsupported query parameter")
+ }
+ }
+ return false, Response{}
+}
+
func (r *Request) getStringParam(param string, defaultValue string) (string, bool) {
q := r.r.URL.Query()
if !q.Has(param) {
@@ -373,10 +383,8 @@ func (r *Request) parseOptStringListParam(param string) ([]string, bool, *Error)
return nil, false, nil
}
for _, value := range q[param] {
- for _, v := range strings.Split(value, ",") {
- if strings.TrimSpace(v) != "" {
- result = append(result, v)
- }
+ for v := range notEmptyString(trimmed(strings.SplitSeq(value, ","))) {
+ result = append(result, v)
}
}
return result, true, nil
@@ -445,6 +453,70 @@ func (r *Request) observeJmapError(jerr jmap.Error) jmap.Error {
return jerr
}
+func (r *Request) needBlob(accountId string) (bool, Response) {
+ if r.session.Capabilities.Blob == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobSessionCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needBlobForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needBlob(accountId); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
+ }
+ if account.AccountCapabilities.Blob == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobAccountCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needBloblWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForBlob()
+ if err != nil {
+ return false, "", r.error(accountId, err)
+ }
+ if ok, resp := r.needBlobForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
+
+func (r *Request) needMail(accountId string) (bool, Response) {
+ if r.session.Capabilities.Mail == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsSessionCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needMailForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needMail(accountId); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
+ }
+ if account.AccountCapabilities.Contacts == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsAccountCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needMailWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForMail()
+ if err != nil {
+ return false, "", r.error(accountId, err)
+ }
+ if ok, resp := r.needMailForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
+
func (r *Request) needTask(accountId string) (bool, Response) {
if !IgnoreSessionCapabilityChecksForTasks {
if r.session.Capabilities.Tasks == nil {
@@ -543,6 +615,38 @@ func (r *Request) needContactWithAccount() (bool, string, Response) {
return true, accountId, Response{}
}
+func (r *Request) needQuota(accountId string) (bool, Response) {
+ if r.session.Capabilities.Quota == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaSessionCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needQuotaForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needQuota(accountId); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
+ }
+ if account.AccountCapabilities.Quota == nil {
+ return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaAccountCapability), r.session.State, jmap.NoLanguage)
+ }
+ return true, Response{}
+}
+
+func (r *Request) needQuotaWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForQuota()
+ if err != nil {
+ return false, "", r.error(accountId, err)
+ }
+ if ok, resp := r.needQuotaForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
+
type SortCrit struct {
Attribute string
Ascending bool
@@ -598,7 +702,3 @@ func mapSort[T any](accountIds []string, req *Request, defaultSort []T, props []
func toState(s string) jmap.State {
return jmap.State(s)
}
-
-func ptr[T any](t T) *T {
- return &t
-}
diff --git a/services/groupware/pkg/groupware/response.go b/services/groupware/pkg/groupware/response.go
index 4fd3369903..aa38d75bee 100644
--- a/services/groupware/pkg/groupware/response.go
+++ b/services/groupware/pkg/groupware/response.go
@@ -74,12 +74,12 @@ func etaggedResponse(accountIds []string, body any, sessionState jmap.SessionSta
}
}
-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) respond(accountId string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, lang jmap.Language) Response {
+ return etaggedResponse(single(accountId), body, sessionState, objectType, etag, lang)
}
-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 (r *Request) respondN(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, lang jmap.Language) Response {
+ return etaggedResponse(accountIds, body, sessionState, objectType, etag, lang)
}
/*
diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go
index 3edbad2681..991bc5992f 100644
--- a/services/groupware/pkg/groupware/route.go
+++ b/services/groupware/pkg/groupware/route.go
@@ -67,6 +67,8 @@ const (
QueryParamQuotas = "quotas"
QueryParamIdentities = "identities"
QueryParamEmailSubmissions = "submissions"
+ QueryParamId = "id"
+ QueryParamCalculateTotal = "total"
HeaderParamSince = "if-none-match"
)
@@ -91,27 +93,31 @@ func (g *Groupware) Route(r chi.Router) {
})
})
r.Route("/{accountid}", func(r chi.Router) {
- r.Get("/", g.GetAccount)
+ r.Get("/", g.GetAccountById)
r.Route("/identities", func(r chi.Router) {
r.Get("/", g.GetIdentities)
- r.Post("/", g.AddIdentity)
+ r.Post("/", g.CreateIdentity)
r.Route("/{identityid}", func(r chi.Router) {
r.Get("/", g.GetIdentityById)
r.Patch("/", g.ModifyIdentity)
r.Delete("/", g.DeleteIdentity)
})
})
- r.Get("/vacation", g.GetVacation)
- r.Put("/vacation", g.SetVacation)
- r.Get("/quota", g.GetQuota)
+ r.Route("/vacation", func(r chi.Router) {
+ r.Get("/", g.GetVacation)
+ r.Put("/", g.SetVacation)
+ })
+ r.Route("/quota", func(r chi.Router) {
+ r.Get("/", g.GetQuota)
+ })
r.Route("/mailboxes", func(r chi.Router) {
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
r.Post("/", g.CreateMailbox)
r.Route("/{mailboxid}", func(r chi.Router) {
- r.Get("/", g.GetMailbox)
- r.Get("/emails", g.GetAllEmailsInMailbox)
- r.Patch("/", g.UpdateMailbox)
+ r.Get("/", g.GetMailboxById)
+ r.Patch("/", g.ModifyMailbox)
r.Delete("/", g.DeleteMailbox)
+ r.Get("/emails", g.GetAllEmailsInMailbox)
})
})
r.Route("/emails", func(r chi.Router) {
@@ -150,7 +156,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetAddressbooks)
r.Post("/", g.CreateAddressBook)
r.Route("/{addressbookid}", func(r chi.Router) {
- r.Get("/", g.GetAddressbook)
+ r.Get("/", g.GetAddressbookById)
r.Patch("/", g.ModifyAddressBook)
r.Delete("/", g.DeleteAddressBook)
r.Get("/contacts", g.GetContactsInAddressbook) //NOSONAR
@@ -176,9 +182,13 @@ func (g *Groupware) Route(r chi.Router) {
})
})
r.Route("/events", func(r chi.Router) {
- r.Post("/", g.CreateCalendarEvent)
- r.Patch("/", g.ModifyCalendarEvent)
- r.Delete("/{eventid}", g.DeleteCalendarEvent)
+ r.Get("/", g.GetAllEvents)
+ r.Post("/", g.CreateEvent)
+ r.Route("/{eventid}", func(r chi.Router) {
+ r.Get("/", g.GetEventById)
+ r.Patch("/", g.ModifyEvent)
+ r.Delete("/", g.DeleteEvent)
+ })
})
r.Route("/tasklists", func(r chi.Router) {
r.Get("/", g.GetTaskLists)
diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go
new file mode 100644
index 0000000000..6b3da5310a
--- /dev/null
+++ b/services/groupware/pkg/groupware/templates.go
@@ -0,0 +1,469 @@
+package groupware
+
+import (
+ "net/http"
+
+ "github.com/opencloud-eu/opencloud/pkg/jmap"
+ "github.com/opencloud-eu/opencloud/pkg/log"
+)
+
+// Create a new {{.Name}} using the JSON payload in the body if the `{{.Method}}` operation.
+//
+// When successful, it returns a `200 OK` with the {{.ObjType}} that was just created in the response.
+func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ bodyFunc func(r Request, accountId string, body *CHANGE, ctx jmap.Context) (bool, Response),
+ createFunc func(accountId string, change CHANGE, ctx jmap.Context) (*T, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ var create CHANGE
+ err := req.body(&create)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+
+ if bodyFunc != nil {
+ if ok, resp := bodyFunc(req, accountId, &create, ctx); !ok {
+ return resp
+ }
+ }
+
+ created, sessionState, state, lang, jerr := createFunc(accountId, create, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, created, sessionState, o.responseType, state, lang)
+ })
+}
+
+func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ if notok, resp := req.unsupportedParams(single(accountId), QueryParamOffset, QueryParamLimit); notok {
+ return resp
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ objs, sessionState, state, lang, jerr := getFunc(accountId, []string{}, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, objs, sessionState, o.responseType, state, lang)
+ })
+}
+
+func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T], FILTER any, COMP any, SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+ filterFunc func(containerId string) FILTER,
+ sortBy []COMP,
+ queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ search := false
+ offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ search = true
+ l = l.Int(QueryParamOffset, offset)
+ }
+
+ limit, ok, err := req.parseUIntParam(QueryParamLimit, uint(0))
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ search = true
+ l = l.Uint(QueryParamLimit, limit)
+ }
+
+ if search {
+ containerId := ""
+ if o.containerUriParamName != "" {
+ var err *Error
+ containerId, err = req.PathParam(o.containerUriParamName)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l = l.Str(o.containerUriParamName, log.SafeString(containerId))
+ }
+
+ filter := filterFunc(containerId)
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, offset, limit, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, results, sessionState, o.responseType, state, lang)
+ } else {
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ objs, sessionState, state, lang, jerr := getFunc(accountId, []string{}, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, objs, sessionState, o.responseType, state, lang)
+ }
+ })
+}
+
+func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ defaultLimit uint,
+ queryFunc func(req Request, accountId string, containerId string, offset int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, *Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ containerId := ""
+ if o.containerUriParamName != "" {
+ var err *Error
+ containerId, err = req.PathParam(o.containerUriParamName)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l = l.Str(o.containerUriParamName, log.SafeString(containerId))
+ }
+
+ offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ l = l.Int(QueryParamOffset, offset)
+ }
+
+ limit, ok, err := req.parseUIntParam(QueryParamLimit, defaultLimit)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ l = l.Uint(QueryParamLimit, limit)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+
+ results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, offset, limit, ctx)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ return req.respond(accountId, results, sessionState, o.responseType, state, lang)
+ })
+}
+
+func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]](
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ ids := []string{}
+ if o.uriParamName != "" {
+ id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(o.uriParamName, log.SafeString(id))
+ ids = single(id)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ objs, sessionState, state, lang, jerr := getFunc(accountId, ids, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+
+ n := len(objs.GetList())
+ switch n {
+ case 0:
+ return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
+ case 1:
+ return req.respond(accountId, objs.GetList()[0], sessionState, ContactResponseObjectType, state, lang)
+ default:
+ logger.Error().Msgf("found %d %s matching '%s' instead of 1", n, o.responseType, ids)
+ return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
+ }
+ })
+}
+
+func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]](
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ getFunc func(accountIds []string, ids []string, ctx jmap.Context) (map[string]RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve")
+ // TODO add id splitting
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(o.uriParamName, log.SafeString(id))
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ objMap, sessionState, state, lang, jerr := getFunc(single(accountId), single(id), ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+
+ if objs, ok := objMap[accountId]; ok {
+ n := len(objs.GetList())
+ switch n {
+ case 0:
+ return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
+ case 1:
+ return req.respond(accountId, objs.GetList()[0], sessionState, ContactResponseObjectType, state, lang)
+ default:
+ logger.Error().Msgf("found %d %s matching '%s' instead of 1", n, o.responseType, id)
+ return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
+ }
+ } else {
+ return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
+ }
+ })
+}
+
+func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ changesFunc func(accountId string, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (CHANGES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ l = l.Uint(QueryParamMaxChanges, maxChanges)
+ }
+
+ sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list changes"))
+ if sinceState != "" {
+ l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ changes, sessionState, state, lang, jerr := changesFunc(accountId, sinceState, maxChanges, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+
+ return req.respond(accountId, changes, sessionState, o.responseType, state, lang)
+ })
+}
+
+func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ deleteFunc func(accountId string, ids []string, ctx jmap.Context) (map[string]jmap.SetError, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to delete")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(o.uriParamName, log.SafeString(id))
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, single(id), ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+
+ for _, e := range setErrors {
+ desc := e.Description
+ if desc != "" {
+ return req.error(accountId, apiError(
+ req.errorId(),
+ o.failedToDeleteError,
+ withDetail(e.Description),
+ ))
+ } else {
+ return req.error(accountId, apiError(
+ req.errorId(),
+ o.failedToDeleteError,
+ ))
+ }
+ }
+ return req.noContent(accountId, sessionState, o.responseType, state)
+ })
+}
+
+func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ deleteFunc func(accountId string, ids []string, ctx jmap.Context) (map[string]jmap.SetError, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+
+ ids := []string{}
+ if o.uriParamName != "" {
+ pathId, err := req.PathParam(o.uriParamName)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ ids = append(ids, pathId)
+ }
+ }
+ {
+ queryIds, ok, err := req.parseOptStringListParam(QueryParamId)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ if ok {
+ ids = append(ids, queryIds...)
+ }
+ }
+ {
+ var bodyIds []string
+ err := req.body(&bodyIds)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ ids = append(ids, bodyIds...)
+ }
+ switch len(ids) {
+ case 0:
+ return req.noop(accountId)
+ case 1:
+ l.Str("id", log.SafeString(ids[0]))
+ default:
+ l.Array("ids", log.SafeStringArray(ids))
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, ids, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+
+ for _, e := range setErrors {
+ desc := e.Description
+ if desc != "" {
+ return req.error(accountId, apiError(
+ req.errorId(),
+ o.failedToDeleteError,
+ withDetail(e.Description),
+ ))
+ } else {
+ return req.error(accountId, apiError(
+ req.errorId(),
+ o.failedToDeleteError,
+ ))
+ }
+ }
+ return req.noContent(accountId, sessionState, o.responseType, state)
+ })
+}
+
+func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
+ o ObjectType[T, CHANGE, CHANGES],
+ w http.ResponseWriter, r *http.Request,
+ g *Groupware,
+ updateFunc func(accountId string, id string, change CHANGE, ctx jmap.Context) (T, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
+) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := o.accountFunc(&req)
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to modify")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(o.uriParamName, log.SafeString(id))
+
+ var change CHANGE
+ err = req.body(&change)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ updated, sessionState, state, lang, jerr := updateFunc(accountId, id, change, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, updated, sessionState, o.responseType, state, lang)
+ })
+}
diff --git a/services/groupware/pkg/groupware/tools.go b/services/groupware/pkg/groupware/tools.go
new file mode 100644
index 0000000000..9e0b512ad7
--- /dev/null
+++ b/services/groupware/pkg/groupware/tools.go
@@ -0,0 +1,20 @@
+package groupware
+
+import (
+ "iter"
+ "strings"
+
+ "github.com/opencloud-eu/opencloud/pkg/structs"
+)
+
+func ptr[T any](t T) *T {
+ return &t
+}
+
+func trimmed(it iter.Seq[string]) iter.Seq[string] {
+ return structs.MapSeq(it, strings.TrimSpace)
+}
+
+func notEmptyString(it iter.Seq[string]) iter.Seq[string] {
+ return structs.FilterSeq(it, func(s string) bool { return s != "" })
+}