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 != "" }) +}