From ce9b4d59bc7e714e40bd34526909784373973afd Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Fri, 24 Apr 2026 14:34:35 +0200
Subject: [PATCH] groupware: fix use of ?limit=0
* JMAP query limit of 0 is synonymous with "no limit", but we actually
want to be able to perform queries without any results, for cases
where we only want to count the total number of objects, and also
because it makes more sense semantically
* introduce query parameter validation checks, in order to only allow
query parameters that are actually supported, which is going to be
useful during development of clients
---
pkg/jmap/api_contact.go | 30 +--
pkg/jmap/api_email.go | 87 ++++---
pkg/jmap/api_event.go | 30 +--
pkg/jmap/api_mailbox.go | 10 +-
pkg/jmap/api_principal.go | 28 ++-
pkg/jmap/integration_calendar_test.go | 20 +-
pkg/jmap/integration_contact_test.go | 10 +-
pkg/jmap/integration_email_test.go | 4 +-
pkg/jmap/integration_test.go | 2 +-
pkg/jmap/model.go | 16 +-
pkg/jmap/model_examples.go | 52 ++--
pkg/jmap/templates.go | 32 ++-
pkg/jmap/tools.go | 39 ++-
pkg/structs/structs.go | 12 +
pkg/structs/structs_test.go | 9 +
.../groupware/pkg/groupware/api_contacts.go | 119 ++++-----
.../groupware/pkg/groupware/api_emails.go | 233 +++++++++---------
.../groupware/pkg/groupware/api_events.go | 104 ++++----
.../groupware/pkg/groupware/api_mailbox.go | 6 +
.../groupware/pkg/groupware/examples_test.go | 13 -
services/groupware/pkg/groupware/objtypes.go | 2 +-
services/groupware/pkg/groupware/request.go | 30 ++-
services/groupware/pkg/groupware/response.go | 4 -
services/groupware/pkg/groupware/templates.go | 106 ++++++--
services/groupware/pkg/groupware/tools.go | 15 ++
25 files changed, 561 insertions(+), 452 deletions(-)
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
+ }
+}