diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 805e8ab66c..4e5d8d0cff 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -64,33 +64,35 @@ func (j *Client) GetContactCardChanges(accountId string, sinceState State, maxCh type ContactCardSearchResults SearchResultsTemplate[ContactCard] -var _ SearchResults[ContactCard] = ContactCardSearchResults{} +var _ SearchResults[ContactCard] = &ContactCardSearchResults{} -func (r ContactCardSearchResults) GetResults() []ContactCard { return r.Results } -func (r ContactCardSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } -func (r ContactCardSearchResults) GetPosition() uint { return r.Position } -func (r ContactCardSearchResults) GetLimit() uint { return r.Limit } -func (r ContactCardSearchResults) GetTotal() *uint { return r.Total } +func (r *ContactCardSearchResults) GetResults() []ContactCard { return r.Results } +func (r *ContactCardSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } +func (r *ContactCardSearchResults) GetPosition() uint { return r.Position } +func (r *ContactCardSearchResults) GetLimit() *uint { return r.Limit } +func (r *ContactCardSearchResults) GetTotal() *uint { return r.Total } +func (r *ContactCardSearchResults) RemoveResults() { r.Results = nil } +func (r *ContactCardSearchResults) SetLimit(limit *uint) { r.Limit = limit } 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) { + position int, limit *uint, calculateTotal bool, + ctx Context) (map[string]*ContactCardSearchResults, SessionState, State, Language, Error) { 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} + func(accountId string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, limit *uint) ContactCardQueryCommand { + return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal} }, func(accountId string, cmd Command, path string, rof string) ContactCardGetRefCommand { return ContactCardGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}} }, - func(query ContactCardQueryResponse, get ContactCardGetResponse) ContactCardSearchResults { - return ContactCardSearchResults{ + func(query ContactCardQueryResponse, get ContactCardGetResponse) *ContactCardSearchResults { + return &ContactCardSearchResults{ Results: get.List, CanCalculateChanges: query.CanCalculateChanges, Position: query.Position, - Total: uintPtrIfPtr(query.Total, calculateTotal), - Limit: query.Limit, + Total: valueIf(query.Total, calculateTotal), + Limit: ptrIf(query.Limit, limit != nil), } }, accountIds, diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index 974d2a2292..d4cb25f7bf 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -113,20 +113,26 @@ func (j *Client) GetEmailBlobId(accountId string, id string, ctx Context) (strin type EmailSearchResults SearchResultsTemplate[Email] -var _ SearchResults[Email] = EmailSearchResults{} +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 } +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 } +func (r *EmailSearchResults) RemoveResults() { r.Results = nil } +func (r *EmailSearchResults) SetLimit(limit *uint) { r.Limit = limit } // Retrieve all the Emails in a given Mailbox by its id. func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR - position int, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool, - ctx Context) (EmailSearchResults, SessionState, State, Language, Error) { + position int, limit *uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool, + ctx Context) (*EmailSearchResults, SessionState, State, Language, Error) { logger := j.loggerParams("GetAllEmailsInMailbox", ctx, func(z zerolog.Context) zerolog.Context { - return z.Bool(logFetchBodies, fetchBodies).Int(logPosition, position).Uint(logLimit, limit) + l := z.Bool(logFetchBodies, fetchBodies).Int(logPosition, position) + if limit != nil { + l = l.Uint(logLimit, *limit) + } + return l }) ctx = ctx.WithLogger(logger) @@ -136,12 +142,8 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}}, CollapseThreads: collapseThreads, CalculateTotal: true, - } - if position > 0 { - query.Position = position - } - if limit > 0 { - query.Limit = &limit + Position: position, + Limit: limit, } get := EmailGetRefCommand{ @@ -173,36 +175,36 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return EmailSearchResults{}, "", "", "", err + return nil, "", "", "", err } - return command(j, ctx, cmd, func(body *Response) (EmailSearchResults, 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 EmailSearchResults{}, "", err + return nil, "", err } var getResponse EmailGetResponse err = retrieveGet(ctx, body, get, "1", &getResponse) if err != nil { logger.Error().Err(err).Send() - return EmailSearchResults{}, "", err + return nil, "", err } if withThreads { var thread ThreadGetResponse err = retrieveGet(ctx, body, threads, "2", &thread) if err != nil { - return EmailSearchResults{}, "", err + return nil, "", err } setThreadSize(&thread, getResponse.List) } - return EmailSearchResults{ + return &EmailSearchResults{ Results: getResponse.List, CanCalculateChanges: queryResponse.CanCalculateChanges, Position: queryResponse.Position, - Limit: queryResponse.Limit, + Limit: ptrIf(queryResponse.Limit, limit != nil), Total: uintPtr(queryResponse.Total), }, queryResponse.QueryState, nil }) @@ -304,10 +306,14 @@ type SearchSnippetWithMeta struct { type EmailSnippetSearchResults SearchResultsTemplate[SearchSnippetWithMeta] func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR - filter EmailFilterElement, position int, limit uint, + filter EmailFilterElement, position int, limit *uint, 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(logPosition, position) + l := z.Int(logPosition, position) + if limit != nil { + l = l.Uint(logLimit, *limit) + } + return l }) ctx = ctx.WithLogger(logger) @@ -320,12 +326,8 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}}, CollapseThreads: true, CalculateTotal: true, - } - if position > 0 { - query.Position = position - } - if limit > 0 { - query.Limit = &limit + Position: position, + Limit: limit, } mails := EmailGetRefCommand{ @@ -409,7 +411,7 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR Results: snippets, CanCalculateChanges: queryResponse.CanCalculateChanges, Total: uintPtr(queryResponse.Total), - Limit: queryResponse.Limit, + Limit: ptrIf(queryResponse.Limit, limit != nil), Position: queryResponse.Position, } } @@ -511,7 +513,7 @@ type EmailQueryWithSnippetsResult struct { } func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR - filter EmailFilterElement, position int, limit uint, collapseThreads bool, calculateTotal bool, fetchBodies bool, maxBodyValueBytes uint, + filter EmailFilterElement, position 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) @@ -527,12 +529,8 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}}, CollapseThreads: collapseThreads, CalculateTotal: calculateTotal, - } - if position > 0 { - query.Position = position - } - if limit > 0 { - query.Limit = &limit + Position: position, + Limit: limit, } snippet := SearchSnippetGetRefCommand{ @@ -1027,7 +1025,7 @@ var EmailSummaryProperties = []string{ } func (j *Client) QueryEmailSummaries(accountIds []string, //NOSONAR - filter EmailFilterElement, limit uint, withThreads bool, + filter EmailFilterElement, limit *uint, withThreads bool, calculateTotal bool, ctx Context) (map[string]EmailsSummary, SessionState, State, Language, Error) { logger := j.logger("QueryEmailSummaries", ctx) ctx = ctx.WithLogger(logger) @@ -1042,12 +1040,11 @@ func (j *Client) QueryEmailSummaries(accountIds []string, //NOSONAR invocations := make([]Invocation, len(uniqueAccountIds)*factor) for i, accountId := range uniqueAccountIds { get := EmailQueryCommand{ - AccountId: accountId, - Filter: filter, - Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}}, - } - if limit > 0 { - get.Limit = &limit + AccountId: accountId, + Filter: filter, + Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}}, + CalculateTotal: calculateTotal, + Limit: limit, } invocations[i*factor+0] = invocation(get, mcid(accountId, "0")) diff --git a/pkg/jmap/api_event.go b/pkg/jmap/api_event.go index 709938d014..1e3edad647 100644 --- a/pkg/jmap/api_event.go +++ b/pkg/jmap/api_event.go @@ -4,13 +4,15 @@ var NS_CALENDAR_EVENTS = ns(JmapCalendars) type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent] -var _ SearchResults[CalendarEvent] = CalendarEventSearchResults{} +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 (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 (r *CalendarEventSearchResults) RemoveResults() { r.Results = nil } +func (r *CalendarEventSearchResults) SetLimit(limit *uint) { r.Limit = limit } func (j *Client) GetCalendarEvents(accountId string, eventIds []string, ctx Context) (CalendarEventGetResponse, SessionState, State, Language, Error) { return get(j, "GetCalendarEvents", CalendarEventType, @@ -26,23 +28,23 @@ func (j *Client) GetCalendarEvents(accountId string, eventIds []string, ctx Cont 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) { + 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, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit *uint) CalendarEventQueryCommand { + return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: 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{ + func(query CalendarEventQueryResponse, get CalendarEventGetResponse) *CalendarEventSearchResults { + return &CalendarEventSearchResults{ Results: get.List, CanCalculateChanges: query.CanCalculateChanges, Position: query.Position, - Total: uintPtrIfPtr(query.Total, calculateTotal), - Limit: query.Limit, + Total: valueIf(query.Total, calculateTotal), + Limit: ptrIf(query.Limit, limit != nil), } }, accountIds, diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index eae723c5c9..c72a23bc7d 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -182,21 +182,21 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOS ) } -func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Context) (map[string][]string, SessionState, State, Language, Error) { +func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Context) (map[string]*[]string, SessionState, State, Language, Error) { return queryN(j, "GetMailboxRolesForMultipleAccounts", MailboxType, []MailboxComparator{{Property: MailboxPropertySortOrder, IsAscending: true}}, - func(accountId string, filter MailboxFilterCondition, sortBy []MailboxComparator, _ int, _ uint) MailboxQueryCommand { + 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 { + func(_ MailboxQueryResponse, get MailboxGetResponse) *[]string { roles := structs.Map(get.List, func(m Mailbox) string { return m.Role }) slices.Sort(roles) - return roles + return &roles }, - accountIds, MailboxFilterCondition{HasAnyRole: boolPtr(true)}, nil, 0, 0, + accountIds, MailboxFilterCondition{HasAnyRole: truep}, nil, nil, 0, ctx, ) } diff --git a/pkg/jmap/api_principal.go b/pkg/jmap/api_principal.go index 7876503faa..e5198a0b43 100644 --- a/pkg/jmap/api_principal.go +++ b/pkg/jmap/api_principal.go @@ -16,33 +16,35 @@ func (j *Client) GetPrincipals(accountId string, ids []string, ctx Context) (Pri type PrincipalSearchResults SearchResultsTemplate[Principal] -var _ SearchResults[Principal] = PrincipalSearchResults{} +var _ SearchResults[Principal] = &PrincipalSearchResults{} -func (r PrincipalSearchResults) GetResults() []Principal { return r.Results } -func (r PrincipalSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } -func (r PrincipalSearchResults) GetPosition() uint { return r.Position } -func (r PrincipalSearchResults) GetLimit() uint { return r.Limit } -func (r PrincipalSearchResults) GetTotal() *uint { return r.Total } +func (r *PrincipalSearchResults) GetResults() []Principal { return r.Results } +func (r *PrincipalSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges } +func (r *PrincipalSearchResults) GetPosition() uint { return r.Position } +func (r *PrincipalSearchResults) GetLimit() *uint { return r.Limit } +func (r *PrincipalSearchResults) GetTotal() *uint { return r.Total } +func (r *PrincipalSearchResults) RemoveResults() { r.Results = nil } +func (r *PrincipalSearchResults) SetLimit(limit *uint) { r.Limit = limit } func (j *Client) QueryPrincipals(accountId string, filter PrincipalFilterElement, sortBy []PrincipalComparator, - position uint, limit uint, calculateTotal bool, - ctx Context) (PrincipalSearchResults, SessionState, State, Language, Error) { + position uint, limit *uint, calculateTotal bool, + ctx Context) (*PrincipalSearchResults, SessionState, State, Language, Error) { return query(j, "QueryPrincipals", PrincipalType, []PrincipalComparator{{Property: PrincipalPropertyName, IsAscending: true}}, - func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit uint) PrincipalQueryCommand { + func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit *uint) PrincipalQueryCommand { return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal} }, func(cmd Command, path string, rof string) PrincipalGetRefCommand { return PrincipalGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}} }, - func(query PrincipalQueryResponse, get PrincipalGetResponse) PrincipalSearchResults { - return PrincipalSearchResults{ + func(query PrincipalQueryResponse, get PrincipalGetResponse) *PrincipalSearchResults { + return &PrincipalSearchResults{ Results: get.List, CanCalculateChanges: query.CanCalculateChanges, Position: query.Position, - Total: uintPtrIf(query.Total, calculateTotal), - Limit: query.Limit, + Total: ptrIf(query.Total, calculateTotal), + Limit: ptrIf(query.Limit, limit != nil), } }, filter, sortBy, limit, position, ctx, diff --git a/pkg/jmap/integration_calendar_test.go b/pkg/jmap/integration_calendar_test.go index 1c4bf56a13..95220c1388 100644 --- a/pkg/jmap/integration_calendar_test.go +++ b/pkg/jmap/integration_calendar_test.go @@ -49,8 +49,8 @@ func TestCalendars(t *testing.T) { //NOSONAR }, func(orig Calendar) CalendarChange { return CalendarChange{ - Description: strPtr(orig.Description + " (changed)"), - IsSubscribed: boolPtr(!orig.IsSubscribed), + Description: ptr(orig.Description + " (changed)"), + IsSubscribed: ptr(!orig.IsSubscribed), } }, func(t *testing.T, orig Calendar, _ CalendarChange, changed Calendar) { @@ -93,7 +93,7 @@ func TestEvents(t *testing.T) { ss := EmptySessionState os := EmptyState { - resultsByAccount, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, 0, true, ctx) + resultsByAccount, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, nil, true, ctx) require.NoError(err) require.Len(resultsByAccount, 1) @@ -124,7 +124,7 @@ func TestEvents(t *testing.T) { for i := range slices { position := int(i * limit) page := min(remainder, limit) - m, sessionState, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, limit, true, ctx) + m, sessionState, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, &limit, true, ctx) require.NoError(err) require.Len(m, 1) require.Contains(m, accountId) @@ -147,7 +147,7 @@ func TestEvents(t *testing.T) { Status: ptr(jscalendar.StatusCancelled), ObjectChange: jscalendar.ObjectChange{ Sequence: uintPtr(99), - ShowWithoutTime: boolPtr(true), + ShowWithoutTime: truep, }, }, } @@ -173,7 +173,7 @@ func TestEvents(t *testing.T) { } { - shouldBeEmpty, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, 0, true, ctx) + shouldBeEmpty, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, nil, true, ctx) require.NoError(err) require.Contains(shouldBeEmpty, accountId) resp := shouldBeEmpty[accountId] @@ -446,13 +446,13 @@ func (s *StalwartTest) fillEvents( //NOSONAR Color: &color, }, Sequence: uintPtr(sequence), - ShowWithoutTime: boolPtr(false), + ShowWithoutTime: falsep, FreeBusyStatus: &freeBusy, Privacy: &privacy, SentBy: organizerEmail, Participants: participantObjs, TimeZone: &tz, - HideAttendees: boolPtr(false), + HideAttendees: falsep, ReplyTo: map[jscalendar.ReplyMethod]string{ jscalendar.ReplyMethodImip: "mailto:" + organizerEmail, //NOSONAR }, @@ -475,8 +475,8 @@ func (s *StalwartTest) fillEvents( //NOSONAR } if EnableEventMayInviteFields { - obj.MayInviteSelf = boolPtr(true) - obj.MayInviteOthers = boolPtr(true) + obj.MayInviteSelf = truep + obj.MayInviteOthers = truep boxes.mayInvite = true } diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_contact_test.go index 1b4fb7c81e..c9e47db610 100644 --- a/pkg/jmap/integration_contact_test.go +++ b/pkg/jmap/integration_contact_test.go @@ -59,8 +59,8 @@ func TestAddressBooks(t *testing.T) { }, func(orig AddressBook) AddressBookChange { return AddressBookChange{ - Description: strPtr(orig.Description + " (changed)"), - IsSubscribed: boolPtr(!orig.IsSubscribed), + Description: ptr(orig.Description + " (changed)"), + IsSubscribed: ptr(!orig.IsSubscribed), } }, func(t *testing.T, orig AddressBook, _ AddressBookChange, changed AddressBook) { @@ -100,7 +100,7 @@ func TestContacts(t *testing.T) { {Property: ContactCardPropertyCreated, IsAscending: true}, } - contactsByAccount, ss, os, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, 0, true, ctx) + contactsByAccount, ss, os, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, nil, true, ctx) require.NoError(err) require.Len(contactsByAccount, 1) @@ -146,7 +146,7 @@ func TestContacts(t *testing.T) { now := time.Now().Truncate(time.Duration(1) * time.Second).UTC() for _, event := range expectedContactCardsById { change := ContactCardChange{ - Language: strPtr("xyz"), + Language: ptr("xyz"), Updated: ptr(now), } changed, sessionState, state, _, err := s.client.UpdateContactCard(accountId, event.Id, change, ctx) @@ -169,7 +169,7 @@ func TestContacts(t *testing.T) { os = state } { - shouldBeEmpty, sessionState, state, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, 0, true, ctx) + shouldBeEmpty, sessionState, state, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, nil, true, ctx) require.NoError(err) require.Contains(shouldBeEmpty, accountId) resp := shouldBeEmpty[accountId] diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go index cc93cb613e..96303ef009 100644 --- a/pkg/jmap/integration_email_test.go +++ b/pkg/jmap/integration_email_test.go @@ -81,7 +81,7 @@ func TestEmails(t *testing.T) { } { - resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, 0, true, false, 0, true, ctx) + resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, nil, true, false, 0, true, ctx) require.NoError(err) require.Equal(session.State, sessionState) @@ -95,7 +95,7 @@ func TestEmails(t *testing.T) { } { - resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, 0, false, false, 0, true, ctx) + resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, nil, false, false, 0, true, ctx) require.NoError(err) require.Equal(session.State, sessionState) diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go index 9c2be773ee..054cc84729 100644 --- a/pkg/jmap/integration_test.go +++ b/pkg/jmap/integration_test.go @@ -1058,7 +1058,7 @@ func toBoolMap[K comparable](s []K) map[K]bool { func toBoolPtrMap[K comparable](s []K) map[K]*bool { m := make(map[K]*bool, len(s)) for _, e := range s { - m[e] = ptr(true) + m[e] = truep } return m } diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 84a5f3ea9f..78e5acc1cb 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -1424,7 +1424,7 @@ type Changes[T Foo] interface { type SearchResultsTemplate[T Foo] struct { // The list of objects that resulted from the query. - Results []T `json:"results"` + Results []T `json:"results,omitempty"` // This is true if the server supports calling queryChanges with these filter/sort parameters. // @@ -1436,10 +1436,10 @@ type SearchResultsTemplate[T Foo] struct { Position uint `json:"position"` // The maximum amount of results to return, as requested using the `limit` query parameter. - Limit uint `json:"limit,omitzero"` + Limit *uint `json:"limit,omitempty"` // The total amount of results that exist for the query. - Total *uint `json:"total,omitzero"` + Total *uint `json:"total,omitempty"` } type SearchResults[T Foo] interface { @@ -1447,7 +1447,9 @@ type SearchResults[T Foo] interface { GetCanCalculateChanges() bool GetPosition() uint GetTotal() *uint - GetLimit() uint + GetLimit() *uint + RemoveResults() + SetLimit(*uint) } type FilterOperatorTerm string @@ -6386,7 +6388,7 @@ type Quota struct { // in the `using` section of the request. // // Further, the server MUST NOT return Quota objects for which there are no types recognized by the client. - Types []ObjectType `json:"types,omitempty"` + Types []ObjectTypeName `json:"types,omitempty"` // The warn limit set by this quota, using the `resourceType` defined as unit of measure. // @@ -7082,7 +7084,7 @@ type ContactCardQueryCommand struct { // 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,omitzero" doc:"opt"` + Limit *uint `json:"limit,omitempty" doc:"opt"` // Does the client wish to know the total number of results in the query? // @@ -8289,7 +8291,7 @@ type PrincipalQueryCommand struct { // 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,omitzero" doc:"opt"` + Limit *uint `json:"limit,omitempty" doc:"opt"` // Does the client wish to know the total number of results in the query? // diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index 7dcf2b4b70..5601caa7d7 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -454,12 +454,12 @@ func (e Exemplar) Quota() Quota { Used: 11696865, HardLimit: 20000000000, Name: e.Username, - Types: []ObjectType{ - EmailType, - SieveScriptType, - FileNodeType, - CalendarEventType, - ContactCardType, + Types: []ObjectTypeName{ + EmailName, + SieveScriptName, + FileNodeName, + CalendarEventName, + ContactCardName, }, Description: e.IdentityName, SoftLimit: 19000000000, @@ -477,12 +477,12 @@ func (e Exemplar) Quotas() []Quota { Used: 29102918, HardLimit: 50000000000, Name: e.SharedAccountId, - Types: []ObjectType{ - EmailType, - SieveScriptType, - FileNodeType, - CalendarEventType, - ContactCardType, + Types: []ObjectTypeName{ + EmailName, + SieveScriptName, + FileNodeName, + CalendarEventName, + ContactCardName, }, Description: e.SharedAccountName, SoftLimit: 90000000000, @@ -562,7 +562,7 @@ func (e Exemplar) MailboxInbox() (Mailbox, string, string) { Id: e.MailboxInboxId, Name: "Inbox", Role: JmapMailboxRoleInbox, - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 1291, UnreadEmails: 82, TotalThreads: 891, @@ -578,7 +578,7 @@ func (e Exemplar) MailboxInbox() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "An Inbox Mailbox", "inbox" } @@ -587,7 +587,7 @@ func (e Exemplar) MailboxInboxProjects() (Mailbox, string, string) { Id: e.MailboxProjectId, ParentId: e.MailboxInboxId, Name: "Projects", - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 112, UnreadEmails: 3, TotalThreads: 85, @@ -603,7 +603,7 @@ func (e Exemplar) MailboxInboxProjects() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "A Projects Mailbox under the Inbox", "projects" } @@ -612,7 +612,7 @@ func (e Exemplar) MailboxDrafts() (Mailbox, string, string) { Id: e.MailboxDraftsId, Name: "Drafts", Role: JmapMailboxRoleDrafts, - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 12, UnreadEmails: 1, TotalThreads: 12, @@ -628,7 +628,7 @@ func (e Exemplar) MailboxDrafts() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "A Drafts Mailbox", "drafts" } @@ -637,7 +637,7 @@ func (e Exemplar) MailboxSent() (Mailbox, string, string) { Id: e.MailboxSentId, Name: "Sent Items", Role: JmapMailboxRoleSent, - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 1621, UnreadEmails: 0, TotalThreads: 1621, @@ -653,7 +653,7 @@ func (e Exemplar) MailboxSent() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "A Sent Mailbox", "sent" } @@ -662,7 +662,7 @@ func (e Exemplar) MailboxJunk() (Mailbox, string, string) { Id: e.MailboxJunkId, Name: "Junk Mail", Role: JmapMailboxRoleJunk, - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 251, UnreadEmails: 0, TotalThreads: 251, @@ -678,7 +678,7 @@ func (e Exemplar) MailboxJunk() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "A Junk Mailbox", "junk" } @@ -687,7 +687,7 @@ func (e Exemplar) MailboxDeleted() (Mailbox, string, string) { Id: e.MailboxDeletedId, Name: "Deleted Items", Role: JmapMailboxRoleTrash, - SortOrder: intPtr(0), + SortOrder: ptr(0), TotalEmails: 99, UnreadEmails: 0, TotalThreads: 91, @@ -703,7 +703,7 @@ func (e Exemplar) MailboxDeleted() (Mailbox, string, string) { MayDelete: true, MaySubmit: true, }, - IsSubscribed: boolPtr(true), + IsSubscribed: truep, }, "A Trash Mailbox", "deleted" } @@ -815,7 +815,7 @@ func (e Exemplar) Emails() EmailSearchResults { return EmailSearchResults{ Results: []Email{e.Email()}, Total: uintPtr(132), - Limit: 1, + Limit: uintPtr(1), Position: 5, CanCalculateChanges: true, } @@ -1991,7 +1991,7 @@ func (e Exemplar) ContactCardChangeForUpdate() (ContactCardChange, string, strin return ContactCardChange{ AddressBookIds: map[string]*bool{ "c34c2bb4-4e8e-4579-b35d-6f6739a11146": nil, - "02b6977f-bb60-4511-949e-37f47a930382": boolPtr(true), + "02b6977f-bb60-4511-949e-37f47a930382": truep, }, Nicknames: map[string]c.Nickname{ "a": { diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 4397c6bec9..57aa11ed2b 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -430,11 +430,11 @@ 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, objType ObjectType, defaultSortBy []SORT, - queryCommandFactory func(filter FILTER, sortBy []SORT, limit uint, position uint) QUERY, + queryCommandFactory func(filter FILTER, sortBy []SORT, position uint, limit *uint) QUERY, getCommandFactory func(cmd Command, path string, rof string) GET, - respMapper func(query QUERYRESP, get GETRESP) RESP, - filter FILTER, sortBy []SORT, limit uint, position uint, - ctx Context) (RESP, SessionState, State, Language, Error) { + respMapper func(query QUERYRESP, get GETRESP) *RESP, + filter FILTER, sortBy []SORT, limit *uint, position uint, + ctx Context) (*RESP, SessionState, State, Language, Error) { logger := client.logger(name, ctx) ctx = ctx.WithLogger(logger) @@ -443,26 +443,24 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T] sortBy = defaultSortBy } - query := queryCommandFactory(filter, sortBy, limit, position) + query := queryCommandFactory(filter, sortBy, position, limit) get := getCommandFactory(query.GetCommand(), "/ids/*", "0") - var zero RESP - cmd, err := client.request(ctx, objType.Namespaces, invocation(query, "0"), invocation(get, "1")) if err != nil { - return zero, "", "", "", err + return nil, "", "", "", err } - return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { + return command(client, ctx, cmd, func(body *Response) (*RESP, State, Error) { var queryResponse QUERYRESP err = retrieveQuery(ctx, body, query, "0", &queryResponse) if err != nil { - return zero, EmptyState, err + return nil, EmptyState, err } var getResponse GETRESP err = retrieveGet(ctx, body, get, "1", &getResponse) if err != nil { - return zero, EmptyState, err + return nil, EmptyState, err } return respMapper(queryResponse, getResponse), queryResponse.GetQueryState(), nil }) @@ -471,12 +469,12 @@ 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, objType ObjectType, defaultSortBy []SORT, - queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, limit uint) QUERY, + queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, limit *uint) QUERY, getCommandFactory func(accountId string, cmd Command, path string, rof string) GET, - respMapper func(query QUERYRESP, get GETRESP) RESP, + respMapper func(query QUERYRESP, get GETRESP) *RESP, accountIds []string, - filter FILTER, sortBy []SORT, limit uint, position int, - ctx Context) (map[string]RESP, SessionState, State, Language, Error) { + 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) @@ -503,8 +501,8 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T return nil, "", "", "", err } - return command(client, ctx, cmd, func(body *Response) (map[string]RESP, State, Error) { - resp := map[string]RESP{} + return command(client, ctx, cmd, func(body *Response) (map[string]*RESP, State, Error) { + resp := map[string]*RESP{} stateByAccountId := map[string]State{} for _, accountId := range uniqueAccountIds { var queryResponse QUERYRESP diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index 677043bb0f..7a2f3ac85f 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -393,22 +393,15 @@ func mapPairs[K comparable, L, R any](left map[K]L, right map[K]R) map[K]pair[L, return result } -func ptr[T any](t T) *T { +var ( + truep = ptr(true) + falsep = ptr(false) +) + +func ptr[T any | string | int | uint | bool](t T) *T { return &t } -func strPtr(s string) *string { - return &s -} - -func intPtr(i int) *int { - return &i -} - -func boolPtr(b bool) *bool { - return &b -} - func identity1[T any](t T) T { return t } @@ -417,24 +410,20 @@ func list[T Foo, GETRESP GetResponse[T]](r GETRESP) []T { return r.GetList() } func getid[T Idable](r T) string { return r.GetId() } func uintPtr(i uint) *uint { - if i > 0 { - return &i + return ptr(i) +} + +func valueIf[T any | uint | int | bool](value *T, condition bool) *T { + if condition { + return value } else { return nil } } -func uintPtrIf(i uint, condition bool) *uint { +func ptrIf[T any | uint | int | bool](value T, condition bool) *T { if condition { - return uintPtr(i) - } else { - return nil - } -} - -func uintPtrIfPtr(i *uint, condition bool) *uint { - if condition { - return i + return &value } else { return nil } diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 57c55592fd..457270aed1 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -82,6 +82,18 @@ func Index[K comparable, V any](source []V, indexer func(V) K) map[K]V { return result } +func Set[V comparable](source []V) map[V]struct{} { + if source == nil { + var zero map[V]struct{} + return zero + } + result := map[V]struct{}{} + for _, v := range source { + result[v] = struct{}{} + } + return result +} + // Creates a slice from a slice, putting each value from the source slice through the // mapper function to determine the value to store into the resulting slice. func Map[E any, R any](source []E, mapper func(E) R) []R { diff --git a/pkg/structs/structs_test.go b/pkg/structs/structs_test.go index a9087fe60f..82ca9034a4 100644 --- a/pkg/structs/structs_test.go +++ b/pkg/structs/structs_test.go @@ -248,3 +248,12 @@ func TestFilterSeq(t *testing.T) { }) } } + +func TestSet(t *testing.T) { + s := Set([]string{"a", "b", "c", "b", "d"}) + assert.Len(t, s, 4) + for _, e := range []string{"a", "b", "c", "d"} { + _, ok := s[e] + assert.True(t, ok) + } +} diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go index 69f0276796..d3d4c22b30 100644 --- a/services/groupware/pkg/groupware/api_contacts.go +++ b/services/groupware/pkg/groupware/api_contacts.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/opencloud-eu/opencloud/pkg/jmap" - "github.com/opencloud-eu/opencloud/pkg/log" ) var ( @@ -42,60 +41,70 @@ var ( // Get all the contacts in an addressbook of an account by its identifier. func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { //NOSONAR - g.respond(w, r, func(req Request) Response { - ok, accountId, resp := req.needContactWithAccount() - if !ok { - return resp - } - accountIds := single(accountId) + getallpaged(Contact, w, r, g, true, + func(addressbookId string) jmap.ContactCardFilterElement { + return jmap.ContactCardFilterCondition{InAddressBook: addressbookId} + }, + []jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyUpdated, IsAscending: true}}, + curryMapQuery(g.jmap.QueryContactCards), + ) - l := req.logger.With() + /* + g.respond(w, r, func(req Request) Response { + ok, accountId, resp := req.needContactWithAccount() + if !ok { + return resp + } + accountIds := single(accountId) - addressBookId, err := req.PathParam(UriParamAddressBookId) - if err != nil { - return req.errorN(accountIds, err) - } - l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) + l := req.logger.With() - position, ok, err := req.parseIntParam(QueryParamPosition, 0) - if err != nil { - return req.errorN(accountIds, err) - } - if ok { - l = l.Int(QueryParamPosition, position) - } + addressBookId, err := req.PathParam(UriParamAddressBookId) + if err != nil { + return req.errorN(accountIds, err) + } + l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) - limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) - if err != nil { - return req.errorN(accountIds, err) - } - if ok { - l = l.Uint(QueryParamLimit, limit) - } + position, ok, err := req.parseIntParam(QueryParamPosition, 0) + if err != nil { + return req.errorN(accountIds, err) + } + if ok { + l = l.Int(QueryParamPosition, position) + } - filter := jmap.ContactCardFilterCondition{ - InAddressBook: addressBookId, - } - var sortBy []jmap.ContactCardComparator - if sort, ok, resp := mapSort(accountIds, &req, DefaultContactSort, SupportedContactSortingProperties, mapContactCardSort); !ok { - return resp - } else { - sortBy = sort - } + limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) + if err != nil { + return req.errorN(accountIds, err) + } + if ok { + l = l.Uint(QueryParamLimit, limit) + } - logger := log.From(l) - ctx := req.ctx.WithLogger(logger) - contactsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryContactCards(accountIds, filter, sortBy, position, limit, true, ctx) - if jerr != nil { - return req.jmapErrorN(accountIds, jerr, sessionState, lang) - } + filter := jmap.ContactCardFilterCondition{ + InAddressBook: addressBookId, + } + var sortBy []jmap.ContactCardComparator + if sort, ok, resp := mapSort(accountIds, &req, DefaultContactSort, SupportedContactSortingProperties, mapContactCardSort); !ok { + return resp + } else { + sortBy = sort + } - if contacts, ok := contactsByAccountId[accountId]; ok { - return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state, lang) - } else { - return req.notFoundN(accountIds, sessionState, ContactResponseObjectType, state) - } - }) + logger := log.From(l) + ctx := req.ctx.WithLogger(logger) + contactsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryContactCards(accountIds, filter, sortBy, position, limit, true, ctx) + if jerr != nil { + return req.jmapErrorN(accountIds, jerr, sessionState, lang) + } + + if contacts, ok := contactsByAccountId[accountId]; ok { + return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state, lang) + } else { + return req.notFoundN(accountIds, sessionState, ContactResponseObjectType, state) + } + }) + */ } func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) { @@ -103,9 +112,9 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) { } func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) { - getallpaged(Contact, w, r, g, - func(cid string) jmap.ContactCardFilterElement { - return jmap.ContactCardFilterCondition{InAddressBook: cid} + getallpaged(Contact, w, r, g, false, + func(_ string) jmap.ContactCardFilterElement { + return jmap.ContactCardFilterCondition{} }, []jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyUpdated, IsAscending: true}}, curryMapQuery(g.jmap.QueryContactCards), @@ -129,11 +138,3 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) { func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) { modify(Contact, w, r, g, g.jmap.UpdateContactCard) } - -func mapContactCardSort(s SortCrit) jmap.ContactCardComparator { - attr := s.Attribute - if mapped, ok := ContactSortingPropertyMapping[s.Attribute]; ok { - attr = mapped - } - return jmap.ContactCardComparator{Property: attr, IsAscending: s.Ascending} -} diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index 1b4e388553..f8e52f1d04 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -42,7 +42,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request fetchBodies := false withThreads := true query(Email, w, r, g, g.defaults.emailLimit, - func(req Request, accountId, containerId string, position int, limit uint, ctx jmap.Context) (jmap.EmailSearchResults, jmap.SessionState, jmap.State, jmap.Language, *Error) { + func(req Request, accountId, containerId string, position 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, position, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx) if jerr != nil { return emails, sessionState, state, lang, req.apiErrorFromJmap(req.observeJmapError(jerr)) @@ -53,7 +53,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request return emails, sessionState, state, lang, err } - safe := jmap.EmailSearchResults{ + safe := &jmap.EmailSearchResults{ Results: sanitized, Total: emails.Total, Limit: emails.Limit, @@ -342,17 +342,12 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since }) } -type EmailSearchSnippetsResults struct { - Results []Snippet `json:"results,omitempty"` - Total uint `json:"total,omitzero"` - Limit uint `json:"limit,omitzero"` - QueryState jmap.State `json:"queryState,omitempty"` -} +type EmailSearchSnippetsResults jmap.SearchResultsTemplate[Snippet] type EmailWithSnippets struct { - AccountId string `json:"accountId,omitempty"` + AccountId string `json:"accountId,omitempty"` + Snippets []SnippetWithoutEmailId `json:"snippets,omitempty"` jmap.Email - Snippets []SnippetWithoutEmailId `json:"snippets,omitempty"` } type Snippet struct { @@ -365,22 +360,9 @@ type SnippetWithoutEmailId struct { Preview string `json:"preview,omitempty"` } -type EmailWithSnippetsSearchResults struct { - Results []EmailWithSnippets `json:"results"` - Total *uint `json:"total,omitzero"` - Position uint `json:"position"` - Limit uint `json:"limit,omitzero"` - QueryState jmap.State `json:"queryState,omitempty"` -} +type EmailWithSnippetsSearchResults jmap.SearchResultsTemplate[EmailWithSnippets] -type EmailSearchResults struct { - Results []jmap.Email `json:"results"` - Total uint `json:"total,omitzero"` - Limit uint `json:"limit,omitzero"` - QueryState jmap.State `json:"queryState,omitempty"` -} - -func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) { //NOSONAR +func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, *uint, *log.Logger, *Error) { //NOSONAR mailboxId, _ := req.getStringParam(QueryParamMailboxId, "") // the identifier of the Mailbox to which to restrict the search text, _ := req.getStringParam(QueryParamSearchText, "") // text that must be included in the Email, specifically in From, To, Cc, Bcc, Subject and any text/* body part from, _ := req.getStringParam(QueryParamSearchFrom, "") // text that must be included in the From header of the Email @@ -392,11 +374,11 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement messageId, _ := req.getStringParam(QueryParamSearchMessageId, "") // value of the Message-ID header of the Email notInMailboxIds, _, err := req.parseOptStringListParam(QueryParamNotInMailboxId) // a comma-separated list of identifiers of Mailboxes the Email must *not* be in if err != nil { - return false, nil, false, 0, 0, nil, err + return false, nil, false, 0, nil, nil, err } keywords, _, err := req.parseOptStringListParam(QueryParamSearchKeyword) // the Email must have all those keywords if err != nil { - return false, nil, false, 0, 0, nil, err + return false, nil, false, 0, nil, nil, err } snippets := false @@ -405,23 +387,27 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement position, ok, err := req.parseIntParam(QueryParamPosition, 0) // pagination element position (offset) if err != nil { - return false, nil, snippets, 0, 0, nil, err + return false, nil, snippets, 0, nil, nil, err } if ok { l = l.Int(QueryParamPosition, position) } - limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) // maximum number of results (size of a page) - if err != nil { - return false, nil, snippets, 0, 0, nil, err - } - if ok { - l = l.Uint(QueryParamLimit, limit) + var limit *uint = nil + { + v, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) // maximum number of results (size of a page) + if err != nil { + return false, nil, snippets, 0, nil, nil, err + } + if ok { + l = l.Uint(QueryParamLimit, v) + limit = &v + } } before, ok, err := req.parseDateParam(QueryParamSearchBefore) // the Email must have been received before this date-time if err != nil { - return false, nil, snippets, 0, 0, nil, err + return false, nil, snippets, 0, nil, nil, err } if ok { l = l.Time(QueryParamSearchBefore, before) @@ -429,7 +415,7 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement after, ok, err := req.parseDateParam(QueryParamSearchAfter) // the Email must have been received after this date-time if err != nil { - return false, nil, snippets, 0, 0, nil, err + return false, nil, snippets, 0, nil, nil, err } if ok { l = l.Time(QueryParamSearchAfter, after) @@ -468,7 +454,7 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0) // the minimum size of the Email if err != nil { - return false, nil, snippets, 0, 0, nil, err + return false, nil, snippets, 0, nil, nil, err } if ok { l = l.Int(QueryParamSearchMinSize, minSize) @@ -476,7 +462,7 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0) // the maximum size of the Email if err != nil { - return false, nil, snippets, 0, 0, nil, err + return false, nil, snippets, 0, nil, nil, err } if ok { l = l.Int(QueryParamSearchMaxSize, maxSize) @@ -575,47 +561,56 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA logger = log.From(l) ctx := req.ctx.WithLogger(logger) - resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, position, limit, collapseThreads, calculateTotal, fetchBodies, g.config.maxBodyValueBytes, ctx) + jmaplimit := limit + if limit != nil && *limit == 0 { + jmaplimit = UintPtrOne + } + + resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, position, jmaplimit, collapseThreads, calculateTotal, fetchBodies, g.config.maxBodyValueBytes, ctx) if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } if results, ok := resultsByAccount[accountId]; ok { - flattened := make([]EmailWithSnippets, len(results.Results)) - for i, result := range results.Results { - var snippets []SnippetWithoutEmailId - if makesSnippets { - snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) - for j, snippet := range result.Snippets { - snippets[j] = SnippetWithoutEmailId{ - Subject: snippet.Subject, - Preview: snippet.Preview, + var flattened []EmailWithSnippets + if limit != nil && *limit == 0 { + flattened = nil + } else { + flattened = make([]EmailWithSnippets, len(results.Results)) + for i, result := range results.Results { + var snippets []SnippetWithoutEmailId + if makesSnippets { + snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) + for j, snippet := range result.Snippets { + snippets[j] = SnippetWithoutEmailId{ + Subject: snippet.Subject, + Preview: snippet.Preview, + } } + } else { + snippets = nil + } + sanitized, err := req.sanitizeEmail(result.Email) + if err != nil { + return req.error(accountId, err) + } + flattened[i] = EmailWithSnippets{ + Email: sanitized, + Snippets: snippets, } - } else { - snippets = nil - } - sanitized, err := req.sanitizeEmail(result.Email) - if err != nil { - return req.error(accountId, err) - } - flattened[i] = EmailWithSnippets{ - Email: sanitized, - Snippets: snippets, } } - var total *uint = nil - if calculateTotal { - total = &results.Total + rlimit := &results.Limit + if limit != nil && *limit == 0 { + rlimit = UintPtrZero } return req.respond(accountId, EmailWithSnippetsSearchResults{ - Results: flattened, - Total: total, - Position: results.Position, - Limit: results.Limit, - QueryState: results.QueryState, + Results: flattened, + Total: ptrIf(results.Total, calculateTotal), + Position: results.Position, + Limit: rlimit, }, sessionState, EmailResponseObjectType, state, lang) } else { return req.notFound(accountId, sessionState, EmailResponseObjectType, state) @@ -639,8 +634,13 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque filter = nil } + jmaplimit := limit + if limit != nil && *limit == 0 { + jmaplimit = UintPtrOne + } + if makesSnippets { - resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, position, limit, ctx) + resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, position, jmaplimit, ctx) if jerr != nil { return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } @@ -654,35 +654,40 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque total += len(results.Results) } - flattened := make([]Snippet, total) - { - i := 0 - for accountId, results := range resultsByAccountId { - for _, result := range results.Results { - flattened[i] = Snippet{ - AccountId: accountId, - SearchSnippetWithMeta: result, + var flattened []Snippet + if limit != nil && *limit == 0 { + flattened = []Snippet{} + } else { + flattened = make([]Snippet, total) + { + i := 0 + for accountId, results := range resultsByAccountId { + for _, result := range results.Results { + flattened[i] = Snippet{ + AccountId: accountId, + SearchSnippetWithMeta: result, + } } } } - } - slices.SortFunc(flattened, func(a, b Snippet) int { return a.ReceivedAt.Compare(b.ReceivedAt) }) + slices.SortFunc(flattened, func(a, b Snippet) int { return a.ReceivedAt.Compare(b.ReceivedAt) }) + } // TODO position and limit over the aggregated results by account body := EmailSearchSnippetsResults{ - Results: flattened, - Total: totalOverAllAccounts, - Limit: limit, - QueryState: state, + Results: flattened, + Total: &totalOverAllAccounts, + Limit: limit, } return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang) } else { withThreads := true + calculateTotal := true - resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, filter, limit, withThreads, ctx) + resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, filter, jmaplimit, withThreads, calculateTotal, ctx) if jerr != nil { return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } @@ -694,27 +699,31 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque total += len(results.Emails) } - flattened := make([]jmap.Email, total) - { - i := 0 - for accountId, results := range resultsByAccountId { - for _, result := range results.Emails { - result.AccountId = accountId - flattened[i] = result - i++ + var flattened []jmap.Email + if limit != nil && *limit == 0 { + flattened = []jmap.Email{} + } else { + flattened = make([]jmap.Email, total) + { + i := 0 + for accountId, results := range resultsByAccountId { + for _, result := range results.Emails { + result.AccountId = accountId + flattened[i] = result + i++ + } } } - } - slices.SortFunc(flattened, func(a, b jmap.Email) int { return a.ReceivedAt.Compare(b.ReceivedAt) }) + slices.SortFunc(flattened, func(a, b jmap.Email) int { return a.ReceivedAt.Compare(b.ReceivedAt) }) + } // TODO position and limit over the aggregated results by account - body := EmailSearchResults{ - Results: flattened, - Total: totalAcrossAllAccounts, - Limit: limit, - QueryState: state, + body := jmap.EmailSearchResults{ + Results: flattened, + Total: &totalAcrossAllAccounts, + Limit: limit, } return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang) @@ -1410,6 +1419,10 @@ type EmailSummary struct { Preview string `json:"preview,omitempty"` } +var _ jmap.Foo = EmailSummary{} + +func (e EmailSummary) GetObjectType() jmap.ObjectType { return jmap.EmailType } + func summarizeEmail(accountId string, email jmap.Email) EmailSummary { return EmailSummary{ AccountId: accountId, @@ -1438,13 +1451,7 @@ type emailWithAccountId struct { email jmap.Email } -type EmailSummaries struct { - Emails []EmailSummary `json:"emails,omitempty"` - Total uint `json:"total,omitzero"` - Limit uint `json:"limit,omitzero"` - Position uint `json:"position,omitzero"` - State jmap.State `json:"state,omitempty"` -} +type EmailSummaries jmap.SearchResultsTemplate[EmailSummary] // Get a summary of the latest emails across all the mailboxes, across all of a user's accounts. // @@ -1513,7 +1520,9 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, logger := log.From(l) ctx := req.ctx.WithLogger(logger) - emailsSummariesByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, filter, limit, true, ctx) + calculateTotal := true + withThreads := true + emailsSummariesByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, filter, &limit, withThreads, calculateTotal, ctx) if jerr != nil { return req.jmapErrorN(allAccountIds, jerr, sessionState, lang) } @@ -1539,12 +1548,16 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, summaries[i] = summarizeEmail(all[i].accountId, all[i].email) } - return req.respondN(allAccountIds, EmailSummaries{ - Emails: summaries, - Total: total, - Limit: limit, + body := EmailSummaries{ + Results: summaries, + Limit: &limit, Position: position, - }, sessionState, EmailResponseObjectType, state, lang) + } + if calculateTotal { + body.Total = &total + } + + return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang) }) } diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go index cca2b63799..b179fd189f 100644 --- a/services/groupware/pkg/groupware/api_events.go +++ b/services/groupware/pkg/groupware/api_events.go @@ -10,60 +10,71 @@ import ( // Get all the events in a calendar of an account by its identifier. func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { //NOSONAR - g.respond(w, r, func(req Request) Response { - ok, accountId, resp := req.needCalendarWithAccount() - if !ok { - return resp - } + getallpaged(Event, w, r, g, + true, + func(calendarId string) jmap.CalendarEventFilterElement { + return jmap.CalendarEventFilterCondition{InCalendar: calendarId} + }, + []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}}, + curryMapQuery(g.jmap.QueryCalendarEvents), + ) - l := req.logger.With() + /* + g.respond(w, r, func(req Request) Response { + ok, accountId, resp := req.needCalendarWithAccount() + if !ok { + return resp + } - calendarId, err := req.PathParam(UriParamCalendarId) - if err != nil { - return req.error(accountId, err) - } - l = l.Str(UriParamCalendarId, log.SafeString(calendarId)) + l := req.logger.With() - position, ok, err := req.parseIntParam(QueryParamPosition, 0) - if err != nil { - return req.error(accountId, err) - } - if ok { - l = l.Int(QueryParamPosition, position) - } + calendarId, err := req.PathParam(UriParamCalendarId) + if err != nil { + return req.error(accountId, err) + } + l = l.Str(UriParamCalendarId, log.SafeString(calendarId)) - limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) - if err != nil { - return req.error(accountId, err) - } - if ok { - l = l.Uint(QueryParamLimit, limit) - } + position, ok, err := req.parseIntParam(QueryParamPosition, 0) + if err != nil { + return req.error(accountId, err) + } + if ok { + l = l.Int(QueryParamPosition, position) + } - filter := jmap.CalendarEventFilterCondition{ - InCalendar: calendarId, - } - sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: false}} + limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.contactLimit) + if err != nil { + return req.error(accountId, err) + } + if ok { + l = l.Uint(QueryParamLimit, limit) + } - logger := log.From(l) - ctx := req.ctx.WithLogger(logger) - eventsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryCalendarEvents(single(accountId), filter, sortBy, position, limit, true, ctx) - if jerr != nil { - return req.jmapError(accountId, jerr, sessionState, lang) - } + filter := jmap.CalendarEventFilterCondition{ + InCalendar: calendarId, + } + sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: false}} - if events, ok := eventsByAccountId[accountId]; ok { - return req.respond(accountId, events, sessionState, EventResponseObjectType, state, lang) - } else { - return req.notFound(accountId, sessionState, EventResponseObjectType, state) - } - }) + logger := log.From(l) + ctx := req.ctx.WithLogger(logger) + eventsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryCalendarEvents(single(accountId), filter, sortBy, position, limit, true, ctx) + if jerr != nil { + return req.jmapError(accountId, jerr, sessionState, lang) + } + + if events, ok := eventsByAccountId[accountId]; ok { + return req.respond(accountId, events, sessionState, EventResponseObjectType, state, lang) + } else { + return req.notFound(accountId, sessionState, EventResponseObjectType, state) + } + }) + */ } 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, position 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, position int, limit uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) { + 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, position 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, position 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, position, limit, true, ctx) return m[accountId], sessionState, state, lang, err } @@ -71,9 +82,8 @@ func curryMapQuery[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any] func (g *Groupware) GetAllEvents(w http.ResponseWriter, r *http.Request) { getallpaged(Event, w, r, g, - func(cid string) jmap.CalendarEventFilterElement { - return jmap.CalendarEventFilterCondition{InCalendar: cid} - }, + false, + func(_ string) jmap.CalendarEventFilterElement { return jmap.CalendarEventFilterCondition{} }, []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}}, curryMapQuery(g.jmap.QueryCalendarEvents), ) diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go index 25fe6ce866..48947bc09c 100644 --- a/services/groupware/pkg/groupware/api_mailbox.go +++ b/services/groupware/pkg/groupware/api_mailbox.go @@ -26,6 +26,8 @@ func (g *Groupware) ModifyMailbox(w http.ResponseWriter, r *http.Request) { modify(Mailbox, w, r, g, g.jmap.UpdateMailbox) } +var GetMailboxesParams = toSupportedQueryParams(QueryParamMailboxSearchName, QueryParamMailboxSearchRole, QueryParamMailboxSearchSubscribed) + // Get the list of all the mailboxes of an account, potentially filtering on the // name and/or role of the mailbox. // @@ -65,6 +67,10 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS hasCriteria = true } + if notok, resp := req.unsupportedQueryParams(single(accountId), GetMailboxesParams); notok { + return resp + } + logger := log.From(req.logger.With().Str(logAccountId, accountId)) ctx := req.ctx.WithLogger(logger) diff --git a/services/groupware/pkg/groupware/examples_test.go b/services/groupware/pkg/groupware/examples_test.go index 69570affd5..0867b6ee2c 100644 --- a/services/groupware/pkg/groupware/examples_test.go +++ b/services/groupware/pkg/groupware/examples_test.go @@ -162,19 +162,6 @@ func (e Exemplar) MailboxesByAccountIdFilteredOnInboxRole() (map[string][]jmap.M }, "All mailboxes for all accounts, filtered on the 'inbox' role", "inboxrole" } -func (e Exemplar) EmailSearchResults() EmailSearchResults { - j := jmap.ExemplarInstance - email := j.Email() - email.BodyStructure = nil - email.BodyValues = nil - return EmailSearchResults{ - Results: []jmap.Email{email}, - Total: 132, - Limit: 1, - QueryState: "seehug3p", - } -} - func (e Exemplar) MailboxRolesByAccounts() (map[string][]string, string, string, string) { j := jmap.ExemplarInstance return map[string][]string{ diff --git a/services/groupware/pkg/groupware/objtypes.go b/services/groupware/pkg/groupware/objtypes.go index d2b2f8b9f6..c52a61032a 100644 --- a/services/groupware/pkg/groupware/objtypes.go +++ b/services/groupware/pkg/groupware/objtypes.go @@ -50,7 +50,7 @@ var ( plural: "contacts", responseType: ContactResponseObjectType, uriParamName: UriParamContactId, - containerUriParamName: UriParamCalendarId, + containerUriParamName: UriParamAddressBookId, accountFunc: (*Request).needCalendarWithAccount, failedToDeleteError: ErrorFailedToDeleteContact, } diff --git a/services/groupware/pkg/groupware/request.go b/services/groupware/pkg/groupware/request.go index 9357c7fe49..258fa08238 100644 --- a/services/groupware/pkg/groupware/request.go +++ b/services/groupware/pkg/groupware/request.go @@ -227,11 +227,19 @@ 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) { +type supportedQueryParams map[string]struct{} + +func toSupportedQueryParams(params ...string) supportedQueryParams { + return structs.Set(params) +} + +var noSupportedQueryParams supportedQueryParams = toSupportedQueryParams() + +func (r *Request) unsupportedQueryParams(accountIds []string, allowed supportedQueryParams) (bool, Response) { q := r.r.URL.Query() - for _, p := range params { - if q.Has(p) { - return true, r.parameterErrorResponse(accountIds, p, "Unsupported query parameter") + for n := range q { + if _, ok := allowed[n]; !ok { + return true, r.parameterErrorResponse(accountIds, n, "Unsupported query parameter") } } return false, Response{} @@ -390,9 +398,11 @@ func (r *Request) parseOptStringListParam(param string) ([]string, bool, *Error) return result, true, nil } +/* func (r *Request) bodydoc(target any, _ string) *Error { return r.body(target) } +*/ func (r *Request) body(target any) *Error { body := r.r.Body @@ -687,18 +697,6 @@ func (r *Request) parseSort(s string, props []string) ([]SortCrit, *Error) { return result, nil } -func mapSort[T any](accountIds []string, req *Request, defaultSort []T, props []string, mapper func(SortCrit) T) ([]T, bool, Response) { - if sortSpec, ok := req.getStringParam(QueryParamSort, ""); ok && strings.TrimSpace(sortSpec) != "" { - if sort, err := req.parseSort(sortSpec, props); err != nil { - return nil, false, errorResponse(accountIds, err, req.session.State, jmap.NoLanguage) - } else { - return structs.Map(sort, mapper), true, Response{} - } - } else { - return defaultSort, true, Response{} - } -} - func toState(s string) jmap.State { return jmap.State(s) } diff --git a/services/groupware/pkg/groupware/response.go b/services/groupware/pkg/groupware/response.go index aa38d75bee..1a7916a0c5 100644 --- a/services/groupware/pkg/groupware/response.go +++ b/services/groupware/pkg/groupware/response.go @@ -170,10 +170,6 @@ func (r *Request) notFound(accountId string, sessionState jmap.SessionState, obj return notFoundResponse(single(accountId), sessionState, objectType, etag) } -func (r *Request) notFoundN(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { - return notFoundResponse(accountIds, sessionState, objectType, etag) -} - func etaggedNotFoundResponse(accountIds []string, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { return Response{ accountIds: accountIds, diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go index 18a95b3360..751d7a8625 100644 --- a/services/groupware/pkg/groupware/templates.go +++ b/services/groupware/pkg/groupware/templates.go @@ -24,6 +24,10 @@ func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( } l := req.logger.With().Str(accountId, log.SafeString(accountId)) + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { + return resp + } + var create CHANGE err := req.body(&create) if err != nil { @@ -62,7 +66,7 @@ func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.G } l := req.logger.With().Str(accountId, log.SafeString(accountId)) - if notok, resp := req.unsupportedParams(single(accountId), QueryParamPosition, QueryParamLimit); notok { + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { return resp } @@ -76,15 +80,18 @@ func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.G }) } +var paginationQueryParams = toSupportedQueryParams(QueryParamPosition, QueryParamLimit) + // Retrieve all the {{.Name}} with support for paging using the {{.QueryParam.QueryParamPosition.Name}} and {{.QueryParam.QueryParamLimit.Name}} query parameters. // @api:response 200:SEARCHRESULTS returns the {{.Names}} within the requested range, as well as the total amount of {{.Names}} func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER any, COMP any, SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR o ObjectType[T, CHANGE, CHANGES], w http.ResponseWriter, r *http.Request, g *Groupware, + withContainerId bool, filterFunc func(containerId string) FILTER, sortBy []COMP, - queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, jmap.Error), + queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, position 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) @@ -101,16 +108,20 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER l = l.Int(QueryParamPosition, position) } - limit, ok, err := req.parseUIntParam(QueryParamLimit, uint(0)) - if err != nil { - return req.error(accountId, err) - } - if ok { - l = l.Uint(QueryParamLimit, limit) + var limit *uint = nil + { + v, ok, err := req.parseUIntParam(QueryParamLimit, uint(0)) + if err != nil { + return req.error(accountId, err) + } + if ok { + l = l.Uint(QueryParamLimit, v) + limit = &v + } } containerId := "" - if o.containerUriParamName != "" { + if withContainerId && o.containerUriParamName != "" { var err *Error containerId, err = req.PathParam(o.containerUriParamName) if err != nil { @@ -119,14 +130,29 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER l = l.Str(o.containerUriParamName, log.SafeString(containerId)) } + if notok, resp := req.unsupportedQueryParams(single(accountId), paginationQueryParams); notok { + return resp + } + filter := filterFunc(containerId) + jmaplimit := limit + if limit != nil && *limit == 0 { + jmaplimit = UintPtrOne + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) - results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, position, limit, ctx) + results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, position, jmaplimit, ctx) if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } + + if limit != nil && *limit == 0 { + results.RemoveResults() + results.SetLimit(UintPtrZero) + } + return req.respond(accountId, results, sessionState, o.responseType, state, lang) }) } @@ -138,7 +164,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT w http.ResponseWriter, r *http.Request, g *Groupware, defaultLimit uint, - queryFunc func(req Request, accountId string, containerId string, position int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, *Error), + queryFunc func(req Request, accountId string, containerId string, position 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) @@ -165,22 +191,38 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT l = l.Int(QueryParamPosition, position) } - limit, ok, err := req.parseUIntParam(QueryParamLimit, defaultLimit) - if err != nil { - return req.error(accountId, err) - } - if ok { - l = l.Uint(QueryParamLimit, limit) + var limit *uint = nil + { + v, ok, err := req.parseUIntParam(QueryParamLimit, defaultLimit) + if err != nil { + return req.error(accountId, err) + } + if ok { + l = l.Uint(QueryParamLimit, v) + limit = &v + } else if defaultLimit > 0 { + limit = &defaultLimit + } } logger := log.From(l) ctx := req.ctx.WithLogger(logger) - results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, position, limit, ctx) + jmaplimit := limit + if limit != nil && *limit == 0 { + jmaplimit = UintPtrOne + } + + results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, position, jmaplimit, ctx) if err != nil { return req.error(accountId, err) } + if limit != nil && *limit == 0 { + results.RemoveResults() + results.SetLimit(UintPtrZero) + } + return req.respond(accountId, results, sessionState, o.responseType, state, lang) }) } @@ -210,6 +252,10 @@ func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetR ids = single(id) } + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { + return resp + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) objs, sessionState, state, lang, jerr := getFunc(accountId, ids, ctx) @@ -251,6 +297,10 @@ func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jm } l.Str(o.uriParamName, log.SafeString(id)) + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { + return resp + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) objMap, sessionState, state, lang, jerr := getFunc(single(accountId), single(id), ctx) @@ -275,6 +325,8 @@ func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jm }) } +var changesSupportedQueryParams = toSupportedQueryParams(QueryParamMaxChanges) + // Retrieve the changes that occured for {{.Name}}, optionally since an opaque state specified using the header `{{.HeaderParam.HeaderParamSince}}`, // optionally bounded by the query parameter `{{.QueryParam.QueryParamMaxChanges}}`. // @api:response 200:CHANGES returns the changes to {{.Names}}: created, updated, and identifiers of destroyed {{.Names}} @@ -304,6 +356,10 @@ func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( l = l.Str(HeaderParamSince, log.SafeString(string(sinceState))) } + if notok, resp := req.unsupportedQueryParams(single(accountId), changesSupportedQueryParams); notok { + return resp + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) changes, sessionState, state, lang, jerr := changesFunc(accountId, sinceState, maxChanges, ctx) @@ -336,6 +392,10 @@ func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR } l.Str(o.uriParamName, log.SafeString(id)) + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { + return resp + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, single(id), ctx) @@ -362,6 +422,8 @@ func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR }) } +var deleteManySupportedQueryParams = toSupportedQueryParams(QueryParamId) + // Delete several {{.Name}} objects referenced by their unique identifiers as specified as an array in the body, // or using the query parameter `{{.QueryParam.QueryParamId}}`. // @api:response 204 when the referenced {{.Names}} have all been deleted successfully @@ -415,6 +477,10 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO l.Array("ids", log.SafeStringArray(ids)) } + if notok, resp := req.unsupportedQueryParams(single(accountId), deleteManySupportedQueryParams); notok { + return resp + } + logger := log.From(l) ctx := req.ctx.WithLogger(logger) setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, ids, ctx) @@ -461,6 +527,10 @@ func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( } l.Str(o.uriParamName, log.SafeString(id)) + if notok, resp := req.unsupportedQueryParams(single(accountId), noSupportedQueryParams); notok { + return resp + } + var change CHANGE err = req.body(&change) if err != nil { diff --git a/services/groupware/pkg/groupware/tools.go b/services/groupware/pkg/groupware/tools.go index 9e0b512ad7..6daef93520 100644 --- a/services/groupware/pkg/groupware/tools.go +++ b/services/groupware/pkg/groupware/tools.go @@ -18,3 +18,18 @@ func trimmed(it iter.Seq[string]) iter.Seq[string] { func notEmptyString(it iter.Seq[string]) iter.Seq[string] { return structs.FilterSeq(it, func(s string) bool { return s != "" }) } + +func uintPtr(v uint) *uint { + return &v +} + +var UintPtrOne *uint = uintPtr(1) +var UintPtrZero *uint = uintPtr(0) + +func ptrIf[T any | uint | int | bool](t T, predicate bool) *T { + if predicate { + return &t + } else { + return nil + } +}