From 8f5fbb00a87e9b517a7a9a15cfd0db8cf08f54fd Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Mon, 13 Apr 2026 09:46:24 +0200 Subject: [PATCH] groupware: refactoring: pass object type instead of namespaces * make the JMAP internal API a bit more future-proof by passing ObjectType objects instead of the JMAP namespaces * remove the new attempt to contain operations even further using the Factory objects * move CalendarEvent operations to its own file, like everything else * fix email tests * ignore WS error when closing an already closed connection --- pkg/jmap/api_addressbook.go | 10 +- pkg/jmap/api_calendar.go | 112 +------------ pkg/jmap/api_contact.go | 10 +- pkg/jmap/api_email.go | 114 ++++++------- pkg/jmap/api_event.go | 105 ++++++++++++ pkg/jmap/api_identity.go | 14 +- pkg/jmap/api_mailbox.go | 118 +++++--------- pkg/jmap/api_principal.go | 4 +- pkg/jmap/api_quota.go | 6 +- pkg/jmap/api_vacation.go | 2 +- pkg/jmap/export_test.go | 59 ++++--- pkg/jmap/http.go | 3 +- ...t_test.go => integration_calendar_test.go} | 2 +- pkg/jmap/integration_contact_test.go | 3 +- pkg/jmap/integration_email_test.go | 26 ++- pkg/jmap/integration_ws_test.go | 11 +- pkg/jmap/model.go | 72 +++++++++ pkg/jmap/model_examples.go | 15 +- pkg/jmap/templates.go | 152 +++++------------- .../groupware/pkg/groupware/api_emails.go | 21 +-- 20 files changed, 423 insertions(+), 436 deletions(-) create mode 100644 pkg/jmap/api_event.go rename pkg/jmap/{integration_event_test.go => integration_calendar_test.go} (99%) diff --git a/pkg/jmap/api_addressbook.go b/pkg/jmap/api_addressbook.go index 299b5ab944..9c1ee8d2ef 100644 --- a/pkg/jmap/api_addressbook.go +++ b/pkg/jmap/api_addressbook.go @@ -3,7 +3,7 @@ package jmap var NS_ADDRESSBOOKS = ns(JmapContacts) func (j *Client) GetAddressbooks(accountId string, ids []string, ctx Context) (AddressBookGetResponse, SessionState, State, Language, Error) { - return get(j, "GetAddressbooks", NS_ADDRESSBOOKS, + return get(j, "GetAddressbooks", MailboxType, func(accountId string, ids []string) AddressBookGetCommand { return AddressBookGetCommand{AccountId: accountId, Ids: ids} }, @@ -19,7 +19,7 @@ type AddressBookChanges = ChangesTemplate[AddressBook] // Retrieve Address Book changes since a given state. // @apidoc addressbook,changes func (j *Client) GetAddressbookChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (AddressBookChanges, SessionState, State, Language, Error) { - return changesA(j, "GetAddressbookChanges", NS_ADDRESSBOOKS, + return changesA(j, "GetAddressbookChanges", MailboxType, func() AddressBookChangesCommand { return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, @@ -50,7 +50,7 @@ func (j *Client) GetAddressbookChanges(accountId string, sinceState State, maxCh } func (j *Client) CreateAddressBook(accountId string, addressbook AddressBookChange, ctx Context) (*AddressBook, SessionState, State, Language, Error) { - return create(j, "CreateAddressBook", NS_ADDRESSBOOKS, + return create(j, "CreateAddressBook", MailboxType, func(accountId string, create map[string]AddressBookChange) AddressBookSetCommand { return AddressBookSetCommand{AccountId: accountId, Create: create} }, @@ -69,7 +69,7 @@ func (j *Client) CreateAddressBook(accountId string, addressbook AddressBookChan } func (j *Client) DeleteAddressBook(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteAddressBook", NS_ADDRESSBOOKS, + return destroy(j, "DeleteAddressBook", MailboxType, func(accountId string, destroy []string) AddressBookSetCommand { return AddressBookSetCommand{AccountId: accountId, Destroy: destroy} }, @@ -80,7 +80,7 @@ func (j *Client) DeleteAddressBook(accountId string, destroyIds []string, ctx Co } func (j *Client) UpdateAddressBook(accountId string, id string, changes AddressBookChange, ctx Context) (AddressBook, SessionState, State, Language, Error) { - return update(j, "UpdateAddressBook", NS_ADDRESSBOOKS, + return update(j, "UpdateAddressBook", MailboxType, func(update map[string]PatchObject) AddressBookSetCommand { return AddressBookSetCommand{AccountId: accountId, Update: update} }, diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 6f13cf7683..536a34e7da 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -24,7 +24,7 @@ func (j *Client) ParseICalendarBlob(accountId string, blobIds []string, ctx Cont } func (j *Client) GetCalendars(accountId string, ids []string, ctx Context) (CalendarGetResponse, SessionState, State, Language, Error) { - return get(j, "GetCalendars", NS_CALENDARS, + return get(j, "GetCalendars", CalendarType, func(accountId string, ids []string) CalendarGetCommand { return CalendarGetCommand{AccountId: accountId, Ids: ids} }, @@ -40,7 +40,7 @@ type CalendarChanges = ChangesTemplate[Calendar] // Retrieve Calendar changes since a given state. // @apidoc calendar,changes func (j *Client) GetCalendarChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (CalendarChanges, SessionState, State, Language, Error) { - return changes(j, "GetCalendarChanges", NS_CALENDARS, + return changes(j, "GetCalendarChanges", CalendarType, func() CalendarChangesCommand { return CalendarChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, @@ -70,110 +70,8 @@ func (j *Client) GetCalendarChanges(accountId string, sinceState State, maxChang ) } -type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent] - -var _ SearchResults[CalendarEvent] = CalendarEventSearchResults{} - -func (r CalendarEventSearchResults) GetResults() []CalendarEvent { return r.Results } -func (r CalendarEventSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } -func (r CalendarEventSearchResults) GetPosition() uint { return r.Position } -func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit } -func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total } - -func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR - filter CalendarEventFilterElement, sortBy []CalendarEventComparator, - position int, limit uint, calculateTotal bool, - ctx Context) (map[string]CalendarEventSearchResults, SessionState, State, Language, Error) { - return queryN(j, "QueryCalendarEvents", NS_CALENDARS, - []CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}}, - func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit uint) CalendarEventQueryCommand { - return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal} - }, - func(accountId string, cmd Command, path string, rof string) CalendarEventGetRefCommand { - return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}} - }, - func(query CalendarEventQueryResponse, get CalendarEventGetResponse) CalendarEventSearchResults { - return CalendarEventSearchResults{ - Results: get.List, - CanCalculateChanges: query.CanCalculateChanges, - Position: query.Position, - Total: uintPtrIf(query.Total, calculateTotal), - Limit: query.Limit, - } - }, - accountIds, - filter, sortBy, limit, position, ctx, - ) -} - -type CalendarEventChanges = ChangesTemplate[CalendarEvent] - -// Retrieve the changes in Calendar Events since a given State. -// @api:tags event,changes -func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, maxChanges uint, - ctx Context) (CalendarEventChanges, SessionState, State, Language, Error) { - return changes(j, "GetCalendarEventChanges", NS_CALENDARS, - func() CalendarEventChangesCommand { - return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} - }, - CalendarEventChangesResponse{}, - func(path string, rof string) CalendarEventGetRefCommand { - return CalendarEventGetRefCommand{ - AccountId: accountId, - IdsRef: &ResultReference{ - Name: CommandCalendarEventChanges, - Path: path, - ResultOf: rof, - }, - } - }, - func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List }, - func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges { - return CalendarEventChanges{ - OldState: oldState, - NewState: newState, - HasMoreChanges: hasMoreChanges, - Created: created, - Updated: updated, - Destroyed: destroyed, - } - }, - ctx, - ) -} - -func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) { - return create(j, "CreateCalendarEvent", NS_CALENDARS, - func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand { - return CalendarEventSetCommand{AccountId: accountId, Create: create} - }, - func(accountId string, ref string) CalendarEventGetCommand { - return CalendarEventGetCommand{AccountId: accountId, Ids: []string{ref}} - }, - func(resp CalendarEventSetResponse) map[string]*CalendarEvent { - return resp.Created - }, - func(resp CalendarEventGetResponse) []CalendarEvent { - return resp.List - }, - accountId, event, - ctx, - ) -} - -func (j *Client) DeleteCalendarEvent(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteCalendarEvent", NS_CALENDARS, - func(accountId string, destroy []string) CalendarEventSetCommand { - return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy} - }, - CalendarEventSetResponse{}, - accountId, destroyIds, - ctx, - ) -} - func (j *Client) CreateCalendar(accountId string, calendar CalendarChange, ctx Context) (*Calendar, SessionState, State, Language, Error) { - return create(j, "CreateCalendar", NS_CALENDARS, + return create(j, "CreateCalendar", CalendarEventType, func(accountId string, create map[string]CalendarChange) CalendarSetCommand { return CalendarSetCommand{AccountId: accountId, Create: create} }, @@ -192,7 +90,7 @@ func (j *Client) CreateCalendar(accountId string, calendar CalendarChange, ctx C } func (j *Client) DeleteCalendar(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteCalendar", NS_CALENDARS, + return destroy(j, "DeleteCalendar", CalendarEventType, func(accountId string, destroy []string) CalendarSetCommand { return CalendarSetCommand{AccountId: accountId, Destroy: destroy} }, @@ -203,7 +101,7 @@ func (j *Client) DeleteCalendar(accountId string, destroyIds []string, ctx Conte } func (j *Client) UpdateCalendar(accountId string, id string, changes CalendarChange, ctx Context) (Calendar, SessionState, State, Language, Error) { - return update(j, "UpdateCalendar", NS_CALENDARS, + return update(j, "UpdateCalendar", CalendarEventType, func(update map[string]PatchObject) CalendarSetCommand { return CalendarSetCommand{AccountId: accountId, Update: update} }, diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 1ae7e2f9e8..9d036e8da1 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -3,7 +3,7 @@ package jmap var NS_CONTACTS = ns(JmapContacts) func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Context) (ContactCardGetResponse, SessionState, State, Language, Error) { - return get(j, "GetContactCards", NS_CONTACTS, + return get(j, "GetContactCards", ContactCardType, func(accountId string, ids []string) ContactCardGetCommand { return ContactCardGetCommand{AccountId: accountId, Ids: contactIds} }, @@ -19,7 +19,7 @@ type ContactCardChanges = ChangesTemplate[ContactCard] // Retrieve the changes in Contact Cards since a given State. // @api:tags contact,changes func (j *Client) GetContactCardChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (ContactCardChanges, SessionState, State, Language, Error) { - return changes(j, "GetContactCardChanges", NS_CONTACTS, + return changes(j, "GetContactCardChanges", ContactCardType, func() ContactCardChangesCommand { return ContactCardChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, @@ -63,7 +63,7 @@ func (j *Client) QueryContactCards(accountIds []string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, limit uint, calculateTotal bool, ctx Context) (map[string]ContactCardSearchResults, SessionState, State, Language, Error) { - return queryN(j, "QueryContactCards", NS_CONTACTS, + return queryN(j, "QueryContactCards", ContactCardType, []ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}}, func(accountId string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, limit uint) ContactCardQueryCommand { return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal} @@ -86,7 +86,7 @@ func (j *Client) QueryContactCards(accountIds []string, } func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Context) (*ContactCard, SessionState, State, Language, Error) { - return create(j, "CreateContactCard", NS_CONTACTS, + return create(j, "CreateContactCard", ContactCardType, func(accountId string, create map[string]ContactCard) ContactCardSetCommand { return ContactCardSetCommand{AccountId: accountId, Create: create} }, @@ -105,7 +105,7 @@ func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Co } func (j *Client) DeleteContactCard(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteContactCard", NS_CONTACTS, + return destroy(j, "DeleteContactCard", ContactCardType, func(accountId string, destroy []string) ContactCardSetCommand { return ContactCardSetCommand{AccountId: accountId, Destroy: destroy} }, diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index d46da23d8f..668823a4f0 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -13,13 +13,6 @@ import ( var NS_MAIL = ns(JmapMail) var NS_MAIL_SUBMISSION = ns(JmapMail, JmapSubmission) -type Emails struct { - Emails []Email `json:"emails,omitempty"` - Total uint `json:"total,omitzero"` - Limit uint `json:"limit,omitzero"` - Offset uint `json:"offset,omitzero"` -} - type getEmailsResult struct { emails []Email notFound []string @@ -32,23 +25,25 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR logger := j.logger("GetEmails", ctx) ctx = ctx.WithLogger(logger) - get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies} + getEmails := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies} if maxBodyValueBytes > 0 { - get.MaxBodyValueBytes = maxBodyValueBytes + getEmails.MaxBodyValueBytes = maxBodyValueBytes } - invokeGet := invocation(get, "1") + invokeGet := invocation(getEmails, "1") methodCalls := []Invocation{invokeGet} + var markEmails EmailSetCommand if markAsSeen { updates := make(map[string]EmailUpdate, len(ids)) for _, id := range ids { updates[id] = EmailUpdate{EmailPropertyKeywords + "/" + JmapKeywordSeen: true} } - mark := EmailSetCommand{AccountId: accountId, Update: updates} - methodCalls = []Invocation{invocation(mark, "0"), invokeGet} + markEmails = EmailSetCommand{AccountId: accountId, Update: updates} + methodCalls = []Invocation{invocation(markEmails, "0"), invokeGet} } + var getThreads ThreadGetRefCommand if withThreads { - threads := ThreadGetRefCommand{ + getThreads = ThreadGetRefCommand{ AccountId: accountId, IdsRef: &ResultReference{ ResultOf: "1", @@ -56,7 +51,7 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR Path: "/list/*/" + EmailPropertyThreadId, //NOSONAR }, } - methodCalls = append(methodCalls, invocation(threads, "2")) + methodCalls = append(methodCalls, invocation(getThreads, "2")) } cmd, err := j.request(ctx, NS_MAIL, methodCalls...) @@ -66,7 +61,7 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR result, sessionState, state, language, gwerr := command(j, ctx, cmd, func(body *Response) (getEmailsResult, State, Error) { if markAsSeen { var markResponse EmailSetResponse - err = retrieveResponseMatchParameters(ctx, body, CommandEmailSet, "0", &markResponse) + err = retrieveSet(ctx, body, markEmails, "0", &markResponse) if err != nil { return getEmailsResult{}, "", err } @@ -76,13 +71,13 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR } } var response EmailGetResponse - err = retrieveResponseMatchParameters(ctx, body, CommandEmailGet, "1", &response) + err = retrieveGet(ctx, body, getEmails, "1", &response) if err != nil { return getEmailsResult{}, "", err } if withThreads { var threads ThreadGetResponse - err = retrieveResponseMatchParameters(ctx, body, CommandThreadGet, "2", &threads) + err = retrieveGet(ctx, body, getThreads, "2", &threads) if err != nil { return getEmailsResult{}, "", err } @@ -116,10 +111,12 @@ func (j *Client) GetEmailBlobId(accountId string, id string, ctx Context) (strin }) } +type EmailSearchResults = SearchResultsTemplate[Email] + // Retrieve all the Emails in a given Mailbox by its id. func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR offset int, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool, - ctx Context) (Emails, SessionState, State, Language, Error) { + ctx Context) (EmailSearchResults, SessionState, State, Language, Error) { logger := j.loggerParams("GetAllEmailsInMailbox", ctx, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Uint(logLimit, limit) }) @@ -168,36 +165,37 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return Emails{}, "", "", "", err + return EmailSearchResults{}, "", "", "", err } - return command(j, ctx, cmd, func(body *Response) (Emails, State, Error) { + return command(j, ctx, cmd, func(body *Response) (EmailSearchResults, State, Error) { var queryResponse EmailQueryResponse err = retrieveQuery(ctx, body, query, "0", &queryResponse) if err != nil { - return Emails{}, "", err + return EmailSearchResults{}, "", err } var getResponse EmailGetResponse err = retrieveGet(ctx, body, get, "1", &getResponse) if err != nil { logger.Error().Err(err).Send() - return Emails{}, "", err + return EmailSearchResults{}, "", err } if withThreads { var thread ThreadGetResponse err = retrieveGet(ctx, body, threads, "2", &thread) if err != nil { - return Emails{}, "", err + return EmailSearchResults{}, "", err } setThreadSize(&thread, getResponse.List) } - return Emails{ - Emails: getResponse.List, - Total: queryResponse.Total, - Limit: queryResponse.Limit, - Offset: queryResponse.Position, + return EmailSearchResults{ + Results: getResponse.List, + CanCalculateChanges: queryResponse.CanCalculateChanges, + Position: queryResponse.Position, + Limit: queryResponse.Limit, + Total: uintPtr(queryResponse.Total), }, queryResponse.QueryState, nil }) } @@ -293,17 +291,11 @@ type SearchSnippetWithMeta struct { SearchSnippet } -type EmailSnippetQueryResult struct { - Snippets []SearchSnippetWithMeta `json:"snippets,omitempty"` - Total uint `json:"total"` - Limit uint `json:"limit,omitzero"` - Position uint `json:"position,omitzero"` - QueryState State `json:"queryState"` -} +type EmailSnippetSearchResults SearchResultsTemplate[SearchSnippetWithMeta] func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR filter EmailFilterElement, offset int, limit uint, - ctx Context) (map[string]EmailSnippetQueryResult, SessionState, State, Language, Error) { + ctx Context) (map[string]EmailSnippetSearchResults, SessionState, State, Language, Error) { logger := j.loggerParams("QueryEmailSnippets", ctx, func(z zerolog.Context) zerolog.Context { return z.Uint(logLimit, limit).Int(logOffset, offset) }) @@ -358,8 +350,9 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR return nil, "", "", "", err } - return command(j, ctx, cmd, func(body *Response) (map[string]EmailSnippetQueryResult, State, Error) { - results := make(map[string]EmailSnippetQueryResult, len(uniqueAccountIds)) + return command(j, ctx, cmd, func(body *Response) (map[string]EmailSnippetSearchResults, State, Error) { + results := make(map[string]EmailSnippetSearchResults, len(uniqueAccountIds)) + states := make(map[string]State, len(uniqueAccountIds)) for _, accountId := range uniqueAccountIds { var queryResponse EmailQueryResponse err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse) @@ -400,15 +393,17 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR i++ } - results[accountId] = EmailSnippetQueryResult{ - Snippets: snippets, - Total: queryResponse.Total, - Limit: queryResponse.Limit, - Position: queryResponse.Position, - QueryState: queryResponse.QueryState, + states[accountId] = queryResponse.QueryState + + results[accountId] = EmailSnippetSearchResults{ + Results: snippets, + CanCalculateChanges: queryResponse.CanCalculateChanges, + Total: uintPtr(queryResponse.Total), + Limit: queryResponse.Limit, + Position: queryResponse.Position, } } - return results, squashStateFunc(results, func(r EmailSnippetQueryResult) State { return r.QueryState }), nil + return results, squashState(states), nil }) } @@ -775,24 +770,15 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, }) } -func (j *Client) DeleteEmails(accountId string, destroy []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - set := EmailSetCommand{ - AccountId: accountId, - Destroy: destroy, - } - cmd, err := j.request(ctx, NS_MAIL, invocation(set, "0")) - if err != nil { - return nil, "", "", "", err - } - - return command(j, ctx, cmd, func(body *Response) (map[string]SetError, State, Error) { - var setResponse EmailSetResponse - err = retrieveSet(ctx, body, set, "0", &setResponse) - if err != nil { - return nil, "", err - } - return setResponse.NotDestroyed, setResponse.NewState, nil - }) +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 { + return EmailSetCommand{AccountId: accountId, Destroy: destroy} + }, + EmailSetResponse{}, + accountId, destroyIds, + ctx, + ) } type SubmittedEmail struct { @@ -1109,7 +1095,7 @@ type EmailSubmissionChanges = ChangesTemplate[EmailSubmission] // @api:tags email,changes func (j *Client) GetEmailSubmissionChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (EmailSubmissionChanges, SessionState, State, Language, Error) { - return changes(j, "GetEmailSubmissionChanges", NS_MAIL_SUBMISSION, + return changes(j, "GetEmailSubmissionChanges", EmailSubmissionType, func() EmailSubmissionChangesCommand { return EmailSubmissionChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, diff --git a/pkg/jmap/api_event.go b/pkg/jmap/api_event.go new file mode 100644 index 0000000000..470e2958ee --- /dev/null +++ b/pkg/jmap/api_event.go @@ -0,0 +1,105 @@ +package jmap + +var NS_CALENDAR_EVENTS = ns(JmapCalendars) + +type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent] + +var _ SearchResults[CalendarEvent] = CalendarEventSearchResults{} + +func (r CalendarEventSearchResults) GetResults() []CalendarEvent { return r.Results } +func (r CalendarEventSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } +func (r CalendarEventSearchResults) GetPosition() uint { return r.Position } +func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit } +func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total } + +func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR + filter CalendarEventFilterElement, sortBy []CalendarEventComparator, + position int, limit uint, calculateTotal bool, + ctx Context) (map[string]CalendarEventSearchResults, SessionState, State, Language, Error) { + return queryN(j, "QueryCalendarEvents", CalendarEventType, + []CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}}, + func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit uint) CalendarEventQueryCommand { + return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal} + }, + func(accountId string, cmd Command, path string, rof string) CalendarEventGetRefCommand { + return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}} + }, + func(query CalendarEventQueryResponse, get CalendarEventGetResponse) CalendarEventSearchResults { + return CalendarEventSearchResults{ + Results: get.List, + CanCalculateChanges: query.CanCalculateChanges, + Position: query.Position, + Total: uintPtrIf(query.Total, calculateTotal), + Limit: query.Limit, + } + }, + accountIds, + filter, sortBy, limit, position, ctx, + ) +} + +type CalendarEventChanges = ChangesTemplate[CalendarEvent] + +// Retrieve the changes in Calendar Events since a given State. +// @api:tags event,changes +func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, maxChanges uint, + ctx Context) (CalendarEventChanges, SessionState, State, Language, Error) { + return changes(j, "GetCalendarEventChanges", CalendarEventType, + func() CalendarEventChangesCommand { + return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} + }, + CalendarEventChangesResponse{}, + func(path string, rof string) CalendarEventGetRefCommand { + return CalendarEventGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandCalendarEventChanges, + Path: path, + ResultOf: rof, + }, + } + }, + func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges { + return CalendarEventChanges{ + OldState: oldState, + NewState: newState, + HasMoreChanges: hasMoreChanges, + Created: created, + Updated: updated, + Destroyed: destroyed, + } + }, + ctx, + ) +} + +func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) { + return create(j, "CreateCalendarEvent", CalendarEventType, + func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand { + return CalendarEventSetCommand{AccountId: accountId, Create: create} + }, + func(accountId string, ref string) CalendarEventGetCommand { + return CalendarEventGetCommand{AccountId: accountId, Ids: []string{ref}} + }, + func(resp CalendarEventSetResponse) map[string]*CalendarEvent { + return resp.Created + }, + func(resp CalendarEventGetResponse) []CalendarEvent { + return resp.List + }, + accountId, event, + ctx, + ) +} + +func (j *Client) DeleteCalendarEvent(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { + return destroy(j, "DeleteCalendarEvent", CalendarEventType, + func(accountId string, destroy []string) CalendarEventSetCommand { + return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy} + }, + CalendarEventSetResponse{}, + accountId, destroyIds, + ctx, + ) +} diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index 12df2eb1a3..e510de4e75 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -9,7 +9,7 @@ import ( var NS_IDENTITY = ns(JmapMail) func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, SessionState, State, Language, Error) { - return getA(j, "GetAllIdentities", NS_IDENTITY, + return getA(j, "GetAllIdentities", IdentityType, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId} }, @@ -20,7 +20,7 @@ func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, Se } func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) ([]Identity, SessionState, State, Language, Error) { - return getA(j, "GetIdentities", NS_IDENTITY, + return getA(j, "GetIdentities", IdentityType, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId, Ids: ids} }, @@ -31,7 +31,7 @@ func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Conte } func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, ctx Context) (map[string][]Identity, SessionState, State, Language, Error) { - return getN(j, "GetIdentitiesForAllAccounts", NS_IDENTITY, + return getN(j, "GetIdentitiesForAllAccounts", IdentityType, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId} }, @@ -96,7 +96,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [ } func (j *Client) CreateIdentity(accountId string, identity IdentityChange, ctx Context) (*Identity, SessionState, State, Language, Error) { - return create(j, "CreateIdentity", NS_IDENTITY, + return create(j, "CreateIdentity", IdentityType, func(accountId string, create map[string]IdentityChange) IdentitySetCommand { return IdentitySetCommand{AccountId: accountId, Create: create} }, @@ -115,7 +115,7 @@ func (j *Client) CreateIdentity(accountId string, identity IdentityChange, ctx C } func (j *Client) UpdateIdentity(accountId string, id string, changes IdentityChange, ctx Context) (Identity, SessionState, State, Language, Error) { - return update(j, "UpdateIdentity", NS_IDENTITY, + return update(j, "UpdateIdentity", IdentityType, func(update map[string]PatchObject) IdentitySetCommand { return IdentitySetCommand{AccountId: accountId, Update: update} }, @@ -130,7 +130,7 @@ func (j *Client) UpdateIdentity(accountId string, id string, changes IdentityCha } func (j *Client) DeleteIdentity(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteIdentity", NS_IDENTITY, + return destroy(j, "DeleteIdentity", IdentityType, func(accountId string, destroy []string) IdentitySetCommand { return IdentitySetCommand{AccountId: accountId, Destroy: destroyIds} }, @@ -146,7 +146,7 @@ type IdentityChanges = ChangesTemplate[Identity] // @api:tags email,changes func (j *Client) GetIdentityChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (IdentityChanges, SessionState, State, Language, Error) { - return changes(j, "GetIdentityChanges", NS_IDENTITY, + return changes(j, "GetIdentityChanges", IdentityType, func() IdentityChangesCommand { return IdentityChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 7e845cf9c7..fc26b5a377 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -9,32 +9,27 @@ import ( var NS_MAILBOX = ns(JmapMail) func (j *Client) GetMailbox(accountId string, ids []string, ctx Context) (MailboxGetResponse, SessionState, State, Language, Error) { - /* - return get(j, "GetMailbox", NS_MAILBOX, - func(accountId string, ids []string) MailboxGetCommand { - return MailboxGetCommand{AccountId: accountId, Ids: ids} - }, - MailboxGetResponse{}, - identity1, - accountId, session, ctx, logger, acceptLanguage, ids, - ) - */ - - return fget[Mailboxes](MAILBOX, j, "GetMailbox", accountId, ids, ctx) + return get(j, "GetMailbox", MailboxType, + func(accountId string, ids []string) MailboxGetCommand { + return MailboxGetCommand{AccountId: accountId, Ids: ids} + }, + MailboxGetResponse{}, + identity1, + accountId, ids, + ctx, + ) } func (j *Client) GetAllMailboxes(accountIds []string, ctx Context) (map[string][]Mailbox, SessionState, State, Language, Error) { - /* - return getAN(j, "GetAllMailboxes", NS_MAILBOX, - func(accountId string, ids []string) MailboxGetCommand { - return MailboxGetCommand{AccountId: accountId} - }, - MailboxGetResponse{}, - identity1, - accountIds, session, ctx, logger, acceptLanguage, []string{}, - ) - */ - return fgetAN[Mailboxes](MAILBOX, j, "GetAllMailboxes", identity1, accountIds, []string{}, ctx) + return getAN(j, "GetAllMailboxes", MailboxType, + func(accountId string, ids []string) MailboxGetCommand { + return MailboxGetCommand{AccountId: accountId} + }, + MailboxGetResponse{}, + identity1, + accountIds, []string{}, + ctx, + ) } func (j *Client) SearchMailboxes(accountIds []string, filter MailboxFilterElement, ctx Context) (map[string][]Mailbox, SessionState, State, Language, Error) { @@ -135,7 +130,7 @@ func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, u // @apidoc mailboxes,changes func (j *Client) GetMailboxChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (MailboxChanges, SessionState, State, Language, Error) { - return changesA(j, "GetMailboxChanges", NS_MAILBOX, + return changesA(j, "GetMailboxChanges", MailboxType, func() MailboxChangesCommand { return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, @@ -161,7 +156,7 @@ func (j *Client) GetMailboxChanges(accountId string, sinceState State, maxChange func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOSONAR sinceStateMap map[string]State, maxChanges uint, ctx Context) (map[string]MailboxChanges, SessionState, State, Language, Error) { - return changesN(j, "GetMailboxChangesForMultipleAccounts", NS_MAILBOX, + return changesN(j, "GetMailboxChangesForMultipleAccounts", MailboxType, accountIds, sinceStateMap, func(accountId string, state State) MailboxChangesCommand { return MailboxChangesCommand{AccountId: accountId, SinceState: state, MaxChanges: uintPtr(maxChanges)} @@ -179,59 +174,22 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOS } func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Context) (map[string][]string, SessionState, State, Language, Error) { - logger := j.logger("GetMailboxRolesForMultipleAccounts", ctx) - ctx = ctx.WithLogger(logger) - - uniqueAccountIds := structs.Uniq(accountIds) - n := len(uniqueAccountIds) - if n < 1 { - return nil, "", "", "", nil - } - - t := true - - invocations := make([]Invocation, n*2) - for i, accountId := range uniqueAccountIds { - invocations[i*2+0] = invocation(MailboxQueryCommand{ - AccountId: accountId, - Filter: MailboxFilterCondition{ - HasAnyRole: &t, - }, - }, mcid(accountId, "0")) - invocations[i*2+1] = invocation(MailboxGetRefCommand{ - AccountId: accountId, - IdsRef: &ResultReference{ - ResultOf: mcid(accountId, "0"), - Name: CommandMailboxQuery, - Path: "/ids", - }, - }, mcid(accountId, "1")) - } - - cmd, err := j.request(ctx, NS_MAILBOX, invocations...) - if err != nil { - return nil, "", "", "", err - } - - return command(j, ctx, cmd, func(body *Response) (map[string][]string, State, Error) { - resp := make(map[string][]string, n) - stateByAccountId := make(map[string]State, n) - for _, accountId := range uniqueAccountIds { - var getResponse MailboxGetResponse - err = retrieveResponseMatchParameters(ctx, body, CommandMailboxGet, mcid(accountId, "1"), &getResponse) - if err != nil { - return nil, "", err - } - roles := make([]string, len(getResponse.List)) - for i, mailbox := range getResponse.List { - roles[i] = mailbox.Role - } + return queryN(j, "GetMailboxRolesForMultipleAccounts", MailboxType, + []MailboxComparator{{Property: MailboxPropertySortOrder, IsAscending: true}}, + func(accountId string, filter MailboxFilterCondition, sortBy []MailboxComparator, _ int, _ uint) MailboxQueryCommand { + return MailboxQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, SortAsTree: false, FilterAsTree: false, Position: 0, Limit: nil, CalculateTotal: false} + }, + func(accountId string, cmd Command, path, rof string) MailboxGetRefCommand { + return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}} + }, + func(_ MailboxQueryResponse, get MailboxGetResponse) []string { + roles := structs.Map(get.List, func(m Mailbox) string { return m.Role }) slices.Sort(roles) - resp[accountId] = roles - stateByAccountId[accountId] = getResponse.State - } - return resp, squashState(stateByAccountId), nil - }) + return roles + }, + accountIds, MailboxFilterCondition{HasAnyRole: boolPtr(true)}, nil, 0, 0, + ctx, + ) } func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, ctx Context) (map[string]string, SessionState, State, Language, Error) { @@ -286,7 +244,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, ctx Contex func (j *Client) UpdateMailbox(accountId string, mailboxId string, change MailboxChange, //NOSONAR ctx Context) (Mailbox, SessionState, State, Language, Error) { - return update(j, "UpdateMailbox", NS_MAILBOX, + return update(j, "UpdateMailbox", MailboxType, func(update map[string]PatchObject) MailboxSetCommand { return MailboxSetCommand{AccountId: accountId, Update: update} }, @@ -301,7 +259,7 @@ func (j *Client) UpdateMailbox(accountId string, mailboxId string, change Mailbo } func (j *Client) CreateMailbox(accountId string, mailbox MailboxChange, ctx Context) (*Mailbox, SessionState, State, Language, Error) { - return create(j, "CreateMailbox", NS_MAILBOX, + return create(j, "CreateMailbox", MailboxType, func(accountId string, create map[string]MailboxChange) MailboxSetCommand { return MailboxSetCommand{AccountId: accountId, Create: create} }, @@ -320,7 +278,7 @@ func (j *Client) CreateMailbox(accountId string, mailbox MailboxChange, ctx Cont } func (j *Client) DeleteMailboxes(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteMailboxes", NS_MAILBOX, + return destroy(j, "DeleteMailboxes", MailboxType, func(accountId string, destroy []string) MailboxSetCommand { return MailboxSetCommand{AccountId: accountId, Destroy: destroyIds} }, diff --git a/pkg/jmap/api_principal.go b/pkg/jmap/api_principal.go index 512c0559df..7876503faa 100644 --- a/pkg/jmap/api_principal.go +++ b/pkg/jmap/api_principal.go @@ -3,7 +3,7 @@ package jmap var NS_PRINCIPALS = ns(JmapPrincipals) func (j *Client) GetPrincipals(accountId string, ids []string, ctx Context) (PrincipalGetResponse, SessionState, State, Language, Error) { - return get(j, "GetPrincipals", NS_PRINCIPALS, + return get(j, "GetPrincipals", PrincipalType, func(accountId string, ids []string) PrincipalGetCommand { return PrincipalGetCommand{AccountId: accountId, Ids: ids} }, @@ -28,7 +28,7 @@ func (j *Client) QueryPrincipals(accountId string, filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit uint, calculateTotal bool, ctx Context) (PrincipalSearchResults, SessionState, State, Language, Error) { - return query(j, "QueryPrincipals", NS_PRINCIPALS, + return query(j, "QueryPrincipals", PrincipalType, []PrincipalComparator{{Property: PrincipalPropertyName, IsAscending: true}}, func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit uint) PrincipalQueryCommand { return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal} diff --git a/pkg/jmap/api_quota.go b/pkg/jmap/api_quota.go index 4aca032636..0e84d562a8 100644 --- a/pkg/jmap/api_quota.go +++ b/pkg/jmap/api_quota.go @@ -3,7 +3,7 @@ package jmap var NS_QUOTA = ns(JmapQuota) func (j *Client) GetQuotas(accountIds []string, ctx Context) (map[string]QuotaGetResponse, SessionState, State, Language, Error) { - return getN(j, "GetQuotas", NS_QUOTA, + return getN(j, "GetQuotas", QuotaType, func(accountId string, ids []string) QuotaGetCommand { return QuotaGetCommand{AccountId: accountId} }, @@ -21,7 +21,7 @@ type QuotaChanges = ChangesTemplate[Quota] // @api:tags quota,changes func (j *Client) GetQuotaChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (QuotaChanges, SessionState, State, Language, Error) { - return changesA(j, "GetQuotaChanges", NS_QUOTA, + return changesA(j, "GetQuotaChanges", QuotaType, func() QuotaChangesCommand { return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, @@ -53,7 +53,7 @@ func (j *Client) GetQuotaChanges(accountId string, sinceState State, maxChanges func (j *Client) GetQuotaUsageChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (QuotaChanges, SessionState, State, Language, Error) { - return updates(j, "GetQuotaUsageChanges", NS_QUOTA, + return updates(j, "GetQuotaUsageChanges", QuotaType, func() QuotaChangesCommand { return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)} }, diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go index 6b1ae9acaf..30625a04d0 100644 --- a/pkg/jmap/api_vacation.go +++ b/pkg/jmap/api_vacation.go @@ -12,7 +12,7 @@ const ( ) func (j *Client) GetVacationResponse(accountId string, ctx Context) (VacationResponseGetResponse, SessionState, State, Language, Error) { - return get(j, "GetVacationResponse", NS_VACATION, + return get(j, "GetVacationResponse", VacationResponseType, func(accountId string, ids []string) VacationResponseGetCommand { return VacationResponseGetCommand{AccountId: accountId} }, diff --git a/pkg/jmap/export_test.go b/pkg/jmap/export_test.go index 094a9ca3bf..bf61f4263c 100644 --- a/pkg/jmap/export_test.go +++ b/pkg/jmap/export_test.go @@ -6,12 +6,35 @@ import ( "fmt" "go/ast" "go/token" + "iter" "log" "strings" "golang.org/x/tools/go/packages" ) +func valuesOf(p *packages.Package) iter.Seq[*ast.ValueSpec] { //NOSONAR + return func(yield func(*ast.ValueSpec) bool) { + for _, syn := range p.Syntax { + for _, decl := range syn.Decls { + g, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + for _, s := range g.Specs { + e, ok := s.(*ast.ValueSpec) + if !ok { + continue + } + if !yield(e) { + return + } + } + } + } + } +} + func parseConsts(pkgID string, suffix string, typeName string) (map[string]string, error) { //NOSONAR result := map[string]string{} { @@ -31,29 +54,19 @@ func parseConsts(pkgID string, suffix string, typeName string) (map[string]strin if p.ID != pkgID { continue } - for _, syn := range p.Syntax { - for _, decl := range syn.Decls { - switch g := decl.(type) { - case *ast.GenDecl: - for _, s := range g.Specs { - switch e := s.(type) { - case *ast.ValueSpec: - for i, ident := range e.Names { - if ident != nil && strings.HasSuffix(ident.Name, suffix) { - value := e.Values[i] - switch c := value.(type) { - case *ast.CallExpr: - switch f := c.Fun.(type) { - case *ast.Ident: - if f.Name == typeName { - switch a := c.Args[0].(type) { - case *ast.BasicLit: - if a.Kind == token.STRING { - result[ident.Name] = strings.Trim(a.Value, `"`) - } - } - } - } + for v := range valuesOf(p) { + for i, ident := range v.Names { + if ident != nil && strings.HasSuffix(ident.Name, suffix) { + value := v.Values[i] + switch c := value.(type) { + case *ast.CallExpr: + switch f := c.Fun.(type) { + case *ast.Ident: + if f.Name == typeName { + switch a := c.Args[0].(type) { + case *ast.BasicLit: + if a.Kind == token.STRING { + result[ident.Name] = strings.Trim(a.Value, `"`) } } } diff --git a/pkg/jmap/http.go b/pkg/jmap/http.go index ec337291a8..ff739c13b2 100644 --- a/pkg/jmap/http.go +++ b/pkg/jmap/http.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httputil" "net/url" @@ -577,7 +578,7 @@ type HttpWsClient struct { func (w *HttpWsClient) readPump() { //NOSONAR logger := log.From(w.logger.With().Str("username", w.username)) defer func() { - if err := w.c.Close(); err != nil { + if err := w.c.Close(); err != nil && !errors.Is(err, net.ErrClosed) { logger.Warn().Err(err).Msg("failed to close websocket connection") } }() diff --git a/pkg/jmap/integration_event_test.go b/pkg/jmap/integration_calendar_test.go similarity index 99% rename from pkg/jmap/integration_event_test.go rename to pkg/jmap/integration_calendar_test.go index 47a8a1623e..cac25266fa 100644 --- a/pkg/jmap/integration_event_test.go +++ b/pkg/jmap/integration_calendar_test.go @@ -126,7 +126,7 @@ func TestEvents(t *testing.T) { results := m[accountId] require.Equal(len(results.Results), int(page)) require.Equal(limit, results.Limit) - require.Equal(position, results.Position) + require.Equal(uint(position), results.Position) require.Equal(true, results.CanCalculateChanges) require.NotNil(results.Total) require.Equal(count, *results.Total) diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_contact_test.go index ef9cef8f6e..7f7ed5975f 100644 --- a/pkg/jmap/integration_contact_test.go +++ b/pkg/jmap/integration_contact_test.go @@ -107,7 +107,8 @@ func TestContacts(t *testing.T) { require.Len(results.Results, int(count)) require.Equal(uint(0), results.Limit) require.Equal(uint(0), results.Position) - require.Equal(uint(0), results.Total) + require.NotNil(results.Total) + require.Equal(count, *results.Total) require.Equal(true, results.CanCalculateChanges) for _, actual := range results.Results { diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go index 3e14d1a356..74aa484fd7 100644 --- a/pkg/jmap/integration_email_test.go +++ b/pkg/jmap/integration_email_test.go @@ -85,8 +85,8 @@ func TestEmails(t *testing.T) { require.NoError(err) require.Equal(session.State, sessionState) - require.Equalf(threads, len(resp.Emails), "the number of collapsed emails in the inbox is expected to be %v, but is actually %v", threads, len(resp.Emails)) - for _, e := range resp.Emails { + require.Equalf(threads, len(resp.Results), "the number of collapsed emails in the inbox is expected to be %v, but is actually %v", threads, len(resp.Results)) + for _, e := range resp.Results { require.Len(e.MessageId, 1) expectation, ok := mailsByMessageId[e.MessageId[0]] require.True(ok) @@ -99,8 +99,8 @@ func TestEmails(t *testing.T) { require.NoError(err) require.Equal(session.State, sessionState) - require.Equalf(count, len(resp.Emails), "the number of emails in the inbox is expected to be %v, but is actually %v", count, len(resp.Emails)) - for _, e := range resp.Emails { + require.Equalf(count, len(resp.Results), "the number of emails in the inbox is expected to be %v, but is actually %v", count, len(resp.Results)) + for _, e := range resp.Results { require.Len(e.MessageId, 1) expectation, ok := mailsByMessageId[e.MessageId[0]] require.True(ok) @@ -168,7 +168,13 @@ func TestSendingEmails(t *testing.T) { accountId string session *Session }{{toAccountId, toSession}, {ccAccountId, ccSession}} { - mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{u.accountId}, ctx) + uctx := Context{ + Session: u.session, + Context: ctx.Context, + Logger: ctx.Logger, + AcceptLanguage: ctx.AcceptLanguage, + } + mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{u.accountId}, uctx) require.NoError(err) for _, mailbox := range mailboxes[u.accountId] { require.Equal(0, mailbox.TotalEmails) @@ -288,7 +294,13 @@ func TestSendingEmails(t *testing.T) { accountId string session *Session }{{to, toAccountId, toSession}, {cc, ccAccountId, ccSession}} { - mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{r.accountId}, ctx) + rctx := Context{ + Session: r.session, + Context: ctx.Context, + Logger: ctx.Logger, + AcceptLanguage: ctx.AcceptLanguage, + } + mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{r.accountId}, rctx) require.NoError(err) inboxId := "" for _, mailbox := range mailboxes[r.accountId] { @@ -299,7 +311,7 @@ func TestSendingEmails(t *testing.T) { } require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.name) - emails, _, _, _, err := s.client.QueryEmails([]string{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, ctx) + emails, _, _, _, err := s.client.QueryEmails([]string{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, rctx) require.NoError(err) require.Contains(emails, r.accountId) require.Len(emails[r.accountId].Emails, 1) diff --git a/pkg/jmap/integration_ws_test.go b/pkg/jmap/integration_ws_test.go index 1a1825d1fb..1d6a8c1bb4 100644 --- a/pkg/jmap/integration_ws_test.go +++ b/pkg/jmap/integration_ws_test.go @@ -1,6 +1,7 @@ package jmap import ( + "slices" "sync" "sync/atomic" "testing" @@ -223,7 +224,15 @@ func TestWs(t *testing.T) { require.Equal(state, changes.NewState) require.Empty(changes.Created) require.Len(changes.Destroyed, 2) - require.EqualValues(emailIds, changes.Destroyed) + { + a := make([]string, len(emailIds)) + copy(a, emailIds) + slices.Sort(emailIds) + b := make([]string, len(changes.Destroyed)) + copy(b, changes.Destroyed) + slices.Sort(changes.Destroyed) + require.EqualValues(a, b) + } require.Empty(changes.Updated) lastState = state } diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 4b28edba03..2c70155fef 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -1564,6 +1564,34 @@ var _ Idable = &Mailbox{} func (f Mailbox) GetObjectType() ObjectType { return MailboxType } func (f Mailbox) GetId() string { return f.Id } +const ( + MailboxPropertyId = "id" + MailboxPropertyName = "name" + MailboxPropertyParentId = "parentId" + MailboxPropertyRole = "role" + MailboxPropertySortOrder = "sortOrder" + MailboxPropertyTotalEmails = "totalEmails" + MailboxPropertyUnreadEmails = "unreadEmails" + MailboxPropertyTotalThreads = "totalThreads" + MailboxPropertyUnreadThreads = "unreadThreads" + MailboxPropertyMyRights = "myRights" + MailboxPropertyIsSubscribed = "isSubscribed" +) + +var MailboxProperties = []string{ + MailboxPropertyId, + MailboxPropertyName, + MailboxPropertyParentId, + MailboxPropertyRole, + MailboxPropertySortOrder, + MailboxPropertyTotalEmails, + MailboxPropertyUnreadEmails, + MailboxPropertyTotalThreads, + MailboxPropertyUnreadThreads, + MailboxPropertyMyRights, + MailboxPropertyIsSubscribed, +} + type MailboxChange struct { // User-visible name for the Mailbox, e.g., “Inbox”. // @@ -1750,6 +1778,49 @@ type MailboxQueryCommand struct { Sort []MailboxComparator `json:"sort,omitempty"` SortAsTree bool `json:"sortAsTree,omitempty"` FilterAsTree bool `json:"filterAsTree,omitempty"` + + // The zero-based index of the first id in the full list of results to return. + // + // If a negative value is given, it is an offset from the end of the list. + // Specifically, the negative value MUST be added to the total number of results given + // the filter, and if still negative, it’s clamped to 0. This is now the zero-based + // index of the first id to return. + // + // If the index is greater than or equal to the total number of objects in the results + // list, then the ids array in the response will be empty, but this is not an error. + Position int `json:"position,omitempty"` + + // An Email id. + // + // If supplied, the position argument is ignored. + // The index of this id in the results will be used in combination with the anchorOffset + // argument to determine the index of the first result to return. + Anchor string `json:"anchor,omitempty"` + + // The index of the first result to return relative to the index of the anchor, + // if an anchor is given. + // + // This MAY be negative. + // + // For example, -1 means the object immediately preceding the anchor is the first result in + // the list returned. + AnchorOffset int `json:"anchorOffset,omitzero" doc:"opt" default:"0"` + + // The maximum number of results to return. + // + // If null, no limit presumed. + // The server MAY choose to enforce a maximum limit argument. + // In this case, if a greater value is given (or if it is null), the limit is clamped + // to the maximum; the new limit is returned with the response so the client is aware. + // + // If a negative value is given, the call MUST be rejected with an invalidArguments error. + Limit *uint `json:"limit,omitempty"` + + // Does the client wish to know the total number of results in the query? + // + // This may be slow and expensive for servers to calculate, particularly with complex filters, + // so clients should take care to only request the total when needed. + CalculateTotal bool `json:"calculateTotal,omitempty"` } var _ QueryCommand[Mailbox] = &MailboxQueryCommand{} @@ -3673,6 +3744,7 @@ var _ GetResponse[Thread] = &ThreadGetResponse{} func (r ThreadGetResponse) GetState() State { return r.State } func (r ThreadGetResponse) GetNotFound() []string { return r.NotFound } func (r ThreadGetResponse) GetList() []Thread { return r.List } +func (r ThreadGetResponse) GetMarker() Thread { return Thread{} } type IdentityGetCommand struct { AccountId string `json:"accountId"` diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index c3a815529c..dec9f8020f 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -806,12 +806,13 @@ func (e Exemplar) EmailBodyPart() EmailBodyPart { } } -func (e Exemplar) Emails() Emails { - return Emails{ - Emails: []Email{e.Email()}, - Total: 132, - Limit: 1, - Offset: 5, +func (e Exemplar) Emails() EmailSearchResults { + return EmailSearchResults{ + Results: []Email{e.Email()}, + Total: uintPtr(132), + Limit: 1, + Position: 5, + CanCalculateChanges: true, } } @@ -820,7 +821,7 @@ func (e Exemplar) EmailGetResponse() EmailGetResponse { AccountId: e.AccountId, State: "aesh2ahj", NotFound: []string{"ahx"}, - List: e.Emails().Emails, + List: e.Emails().Results, } } diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 345a005443..421d249a1e 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -8,72 +8,8 @@ import ( "github.com/rs/zerolog" ) -type Factory[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any] interface { - Namespaces() []JmapNamespace - CreateGetCommand(accountId string, ids []string) GETREQ - CreateGetResponse() GETRESP - MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []T, destroyed []string) CHANGES -} - -type Mailboxes string - -const MAILBOX = Mailboxes("MAILBOX") - -var _ Factory[Mailbox, MailboxGetCommand, MailboxGetResponse, MailboxChanges] = MAILBOX - -func (f Mailboxes) Namespaces() []JmapNamespace { - return NS_MAILBOX -} - -func (f Mailboxes) CreateGetCommand(accountId string, ids []string) MailboxGetCommand { - return MailboxGetCommand{AccountId: accountId, Ids: ids} -} - -func (f Mailboxes) CreateGetResponse() MailboxGetResponse { - return MailboxGetResponse{} -} - -func (f Mailboxes) MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges { - return MailboxChanges{ - OldState: oldState, - NewState: newState, - HasMoreChanges: hasMoreChanges, - Created: created, - Updated: updated, - Destroyed: destroyed, - } -} - -func fget[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR - client *Client, name string, - accountId string, ids []string, - ctx Context) (GETRESP, SessionState, State, Language, Error) { - var getresp GETRESP - return get(client, name, f.Namespaces(), - f.CreateGetCommand, - getresp, - identity1, - accountId, ids, - ctx, - ) -} - -func fgetA[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR - client *Client, name string, - accountId string, ids []string, - ctx Context) ([]T, SessionState, State, Language, Error) { - var getresp GETRESP - return getA(client, name, f.Namespaces(), - f.CreateGetCommand, - getresp, - accountId, - ids, - ctx, - ) -} - func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, getCommandFactory func(string, []string) GETREQ, _ GETRESP, mapper func(GETRESP) RESP, @@ -83,7 +19,7 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSON var zero RESP get := getCommandFactory(accountId, ids) - cmd, err := client.request(ctx, using, invocation(get, "0")) + cmd, err := client.request(ctx, objType.Namespaces, invocation(get, "0")) if err != nil { return zero, "", "", "", err } @@ -100,35 +36,20 @@ 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, using []JmapNamespace, + 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, using, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, ids, ctx) -} - -func fgetAN[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any, CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR - client *Client, name string, - respMapper func(map[string][]T) RESP, - accountIds []string, ids []string, - ctx Context) (RESP, SessionState, State, Language, Error) { - var getresp GETRESP - return getAN(client, name, f.Namespaces(), - f.CreateGetCommand, - getresp, - respMapper, - accountIds, ids, - ctx, - ) + 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, using []JmapNamespace, + client *Client, name string, objType ObjectType, getCommandFactory func(string, []string) GETREQ, resp GETRESP, respMapper func(map[string][]T) RESP, accountIds []string, ids []string, ctx Context) (RESP, SessionState, State, Language, Error) { - return getN(client, name, using, getCommandFactory, resp, + return getN(client, name, objType, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, respMapper, accountIds, ids, @@ -137,7 +58,7 @@ func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOS } func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, getCommandFactory func(string, []string) GETREQ, _ GETRESP, itemMapper func(GETRESP) ITEM, @@ -158,7 +79,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP an invocations[i] = invocation(get, mcid(accountId, "0")) } - cmd, err := client.request(ctx, using, invocations...) + cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { return zero, "", "", "", err } @@ -180,7 +101,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP an } func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, setCommandFactory func(string, map[string]C) SETREQ, getCommandFactory func(string, string) GETREQ, createdMapper func(SETRESP) map[string]*T, @@ -193,7 +114,7 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se createMap := map[string]C{"c": create} get := getCommandFactory(accountId, "#c") set := setCommandFactory(accountId, createMap) - cmd, err := client.request(ctx, using, + cmd, err := client.request(ctx, objType.Namespaces, invocation(set, "0"), invocation(get, "1"), ) @@ -240,14 +161,14 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se }) } -func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR +func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, objType ObjectType, //NOSONAR setCommandFactory func(string, []string) REQ, _ RESP, accountId string, destroy []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) { logger := client.logger(name, ctx) ctx = ctx.WithLogger(logger) set := setCommandFactory(accountId, destroy) - cmd, err := client.request(ctx, using, + cmd, err := client.request(ctx, objType.Namespaces, invocation(set, "0"), ) if err != nil { @@ -265,7 +186,7 @@ func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name } func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, changesCommandFactory func() CHANGESREQ, changesResp CHANGESRESP, _ GETRESP, @@ -273,7 +194,7 @@ func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES respMapper func(State, State, bool, []T, []T, []string) RESP, ctx Context) (RESP, SessionState, State, Language, Error) { - return changes(client, name, using, changesCommandFactory, changesResp, getCommandFactory, + return changes(client, name, objType, changesCommandFactory, changesResp, getCommandFactory, func(r GETRESP) []T { return r.GetList() }, respMapper, ctx, @@ -281,7 +202,7 @@ func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES } func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, changesCommandFactory func() CHANGESREQ, _ CHANGESRESP, getCommandFactory func(string, string) GETREQ, @@ -295,7 +216,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR getCreated := getCommandFactory("/created", "0") //NOSONAR getUpdated := getCommandFactory("/updated", "0") //NOSONAR - cmd, err := client.request(ctx.WithLogger(logger), using, + cmd, err := client.request(ctx.WithLogger(logger), objType.Namespaces, invocation(changes, "0"), invocation(getCreated, "1"), invocation(getUpdated, "2"), @@ -335,7 +256,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR } func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, CHANGESITEM any, RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, accountIds []string, sinceStateMap map[string]State, changesCommandFactory func(string, State) CHANGESREQ, _ CHANGESRESP, @@ -362,8 +283,9 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES } invocations := make([]Invocation, n*3) - getCommand := Command("") - changesCommand := Command("") + var ch CHANGESREQ + var gc GETREQ + var gu GETREQ for i, accountId := range uniqueAccountIds { sinceState, ok := sinceStateMap[accountId] if !ok { @@ -379,13 +301,14 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES invocations[i*3+1] = invocation(getCreated, mcid(accountId, "1")) invocations[i*3+2] = invocation(getUpdated, mcid(accountId, "2")) - changesCommand = changes.GetCommand() - getCommand = getCreated.GetCommand() + ch = changes + gc = getCreated + gu = getUpdated } ctx = ctx.WithLogger(logger) - cmd, err := client.request(ctx, using, invocations...) + cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { return zero, "", "", "", err } @@ -395,19 +318,19 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES stateByAccountId := make(map[string]State, n) for _, accountId := range uniqueAccountIds { var changesResponse CHANGESRESP - err = retrieveResponseMatchParameters(ctx, body, changesCommand, mcid(accountId, "0"), &changesResponse) + err = retrieveChanges(ctx, body, ch, mcid(accountId, "0"), &changesResponse) if err != nil { return zero, "", err } var createdResponse GETRESP - err = retrieveResponseMatchParameters(ctx, body, getCommand, mcid(accountId, "1"), &createdResponse) + err = retrieveGet(ctx, body, gc, mcid(accountId, "1"), &createdResponse) if err != nil { return zero, "", err } var updatedResponse GETRESP - err = retrieveResponseMatchParameters(ctx, body, getCommand, mcid(accountId, "2"), &updatedResponse) + err = retrieveGet(ctx, body, gu, mcid(accountId, "2"), &updatedResponse) if err != nil { return zero, "", err } @@ -422,7 +345,7 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES } func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, changesCommandFactory func() CHANGESREQ, _ CHANGESRESP, getCommandFactory func(string, string) GETREQ, @@ -435,7 +358,7 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR changes := changesCommandFactory() getUpdated := getCommandFactory("/updated", "0") //NOSONAR - cmd, err := client.request(ctx, using, + cmd, err := client.request(ctx, objType.Namespaces, invocation(changes, "0"), invocation(getUpdated, "1"), ) @@ -464,7 +387,8 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR }) } -func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR +func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR + client *Client, name string, objType ObjectType, setCommandFactory func(map[string]PatchObject) SET, getCommandFactory func(string) GET, notUpdatedExtractor func(SETRESP) map[string]SetError, @@ -473,9 +397,10 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an ctx Context) (RESP, SessionState, State, Language, Error) { logger := client.logger(name, ctx) ctx = ctx.WithLogger(logger) + update := setCommandFactory(map[string]PatchObject{id: changes.AsPatch()}) get := getCommandFactory(id) - cmd, err := client.request(ctx, using, invocation(update, "0"), invocation(get, "1")) + cmd, err := client.request(ctx, objType.Namespaces, invocation(update, "0"), invocation(get, "1")) var zero RESP if err != nil { return zero, "", "", "", err @@ -503,7 +428,7 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an } func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, defaultSortBy []SORT, queryCommandFactory func(filter FILTER, sortBy []SORT, limit uint, position uint) QUERY, getCommandFactory func(cmd Command, path string, rof string) GET, @@ -523,7 +448,7 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T] var zero RESP - cmd, err := client.request(ctx, using, invocation(query, "0"), invocation(get, "1")) + cmd, err := client.request(ctx, objType.Namespaces, invocation(query, "0"), invocation(get, "1")) if err != nil { return zero, "", "", "", err } @@ -544,7 +469,7 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T] } func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR - client *Client, name string, using []JmapNamespace, + client *Client, name string, objType ObjectType, defaultSortBy []SORT, queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, limit uint) QUERY, getCommandFactory func(accountId string, cmd Command, path string, rof string) GET, @@ -552,6 +477,9 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T accountIds []string, filter FILTER, sortBy []SORT, limit uint, position int, ctx Context) (map[string]RESP, SessionState, State, Language, Error) { + logger := client.logger(name, ctx) + ctx = ctx.WithLogger(logger) + uniqueAccountIds := structs.Uniq(accountIds) if sortBy == nil { @@ -570,7 +498,7 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T g = get } - cmd, err := client.request(ctx, NS_CALENDARS, invocations...) + cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { return nil, "", "", "", err } diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index de781c39c3..e8b791473a 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -109,16 +109,17 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request return req.jmapError(accountId, jerr, sessionState, lang) } - sanitized, err := req.sanitizeEmails(emails.Emails) + sanitized, err := req.sanitizeEmails(emails.Results) if err != nil { return req.error(accountId, err) } - safe := jmap.Emails{ - Emails: sanitized, - Total: emails.Total, - Limit: emails.Limit, - Offset: emails.Offset, + 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) @@ -689,15 +690,17 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque var totalOverAllAccounts uint = 0 total := 0 for _, results := range resultsByAccountId { - totalOverAllAccounts += results.Total - total += len(results.Snippets) + if results.Total != nil { + totalOverAllAccounts += *results.Total + } + total += len(results.Results) } flattened := make([]Snippet, total) { i := 0 for accountId, results := range resultsByAccountId { - for _, result := range results.Snippets { + for _, result := range results.Results { flattened[i] = Snippet{ AccountId: accountId, SearchSnippetWithMeta: result,