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
This commit is contained in:
Pascal Bleser
2026-04-24 14:34:35 +02:00
parent 652b87d350
commit d88a68d1cb
25 changed files with 561 additions and 452 deletions
+16 -14
View File
@@ -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,
+42 -45
View File
@@ -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"))
+16 -14
View File
@@ -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,
+5 -5
View File
@@ -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,
)
}
+15 -13
View File
@@ -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,
+10 -10
View File
@@ -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
}
+5 -5
View File
@@ -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]
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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
}
+9 -7
View File
@@ -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?
//
+26 -26
View File
@@ -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": {
+15 -17
View File
@@ -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
+14 -25
View File
@@ -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
}
+12
View File
@@ -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 {
+9
View File
@@ -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)
}
}
@@ -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}
}
+123 -110
View File
@@ -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)
})
}
+57 -47
View File
@@ -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),
)
@@ -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)
@@ -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{
+1 -1
View File
@@ -50,7 +50,7 @@ var (
plural: "contacts",
responseType: ContactResponseObjectType,
uriParamName: UriParamContactId,
containerUriParamName: UriParamCalendarId,
containerUriParamName: UriParamAddressBookId,
accountFunc: (*Request).needCalendarWithAccount,
failedToDeleteError: ErrorFailedToDeleteContact,
}
+14 -16
View File
@@ -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)
}
@@ -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,
+88 -18
View File
@@ -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 {
+15
View File
@@ -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
}
}