groupware: refactoring: pass object type instead of namespaces

* make the JMAP internal API a bit more future-proof by passing
   ObjectType objects instead of the JMAP namespaces

 * remove the new attempt to contain operations even further using the
   Factory objects

 * move CalendarEvent operations to its own file, like everything else

 * fix email tests

 * ignore WS error when closing an already closed connection
This commit is contained in:
Pascal Bleser
2026-04-13 09:46:24 +02:00
parent b6cedcbe90
commit 8f5fbb00a8
20 changed files with 423 additions and 436 deletions

View File

@@ -3,7 +3,7 @@ package jmap
var NS_ADDRESSBOOKS = ns(JmapContacts)
func (j *Client) GetAddressbooks(accountId string, ids []string, ctx Context) (AddressBookGetResponse, SessionState, State, Language, Error) {
return get(j, "GetAddressbooks", NS_ADDRESSBOOKS,
return get(j, "GetAddressbooks", MailboxType,
func(accountId string, ids []string) AddressBookGetCommand {
return AddressBookGetCommand{AccountId: accountId, Ids: ids}
},
@@ -19,7 +19,7 @@ type AddressBookChanges = ChangesTemplate[AddressBook]
// Retrieve Address Book changes since a given state.
// @apidoc addressbook,changes
func (j *Client) GetAddressbookChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (AddressBookChanges, SessionState, State, Language, Error) {
return changesA(j, "GetAddressbookChanges", NS_ADDRESSBOOKS,
return changesA(j, "GetAddressbookChanges", MailboxType,
func() AddressBookChangesCommand {
return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
@@ -50,7 +50,7 @@ func (j *Client) GetAddressbookChanges(accountId string, sinceState State, maxCh
}
func (j *Client) CreateAddressBook(accountId string, addressbook AddressBookChange, ctx Context) (*AddressBook, SessionState, State, Language, Error) {
return create(j, "CreateAddressBook", NS_ADDRESSBOOKS,
return create(j, "CreateAddressBook", MailboxType,
func(accountId string, create map[string]AddressBookChange) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Create: create}
},
@@ -69,7 +69,7 @@ func (j *Client) CreateAddressBook(accountId string, addressbook AddressBookChan
}
func (j *Client) DeleteAddressBook(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteAddressBook", NS_ADDRESSBOOKS,
return destroy(j, "DeleteAddressBook", MailboxType,
func(accountId string, destroy []string) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Destroy: destroy}
},
@@ -80,7 +80,7 @@ func (j *Client) DeleteAddressBook(accountId string, destroyIds []string, ctx Co
}
func (j *Client) UpdateAddressBook(accountId string, id string, changes AddressBookChange, ctx Context) (AddressBook, SessionState, State, Language, Error) {
return update(j, "UpdateAddressBook", NS_ADDRESSBOOKS,
return update(j, "UpdateAddressBook", MailboxType,
func(update map[string]PatchObject) AddressBookSetCommand {
return AddressBookSetCommand{AccountId: accountId, Update: update}
},

View File

@@ -24,7 +24,7 @@ func (j *Client) ParseICalendarBlob(accountId string, blobIds []string, ctx Cont
}
func (j *Client) GetCalendars(accountId string, ids []string, ctx Context) (CalendarGetResponse, SessionState, State, Language, Error) {
return get(j, "GetCalendars", NS_CALENDARS,
return get(j, "GetCalendars", CalendarType,
func(accountId string, ids []string) CalendarGetCommand {
return CalendarGetCommand{AccountId: accountId, Ids: ids}
},
@@ -40,7 +40,7 @@ type CalendarChanges = ChangesTemplate[Calendar]
// Retrieve Calendar changes since a given state.
// @apidoc calendar,changes
func (j *Client) GetCalendarChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (CalendarChanges, SessionState, State, Language, Error) {
return changes(j, "GetCalendarChanges", NS_CALENDARS,
return changes(j, "GetCalendarChanges", CalendarType,
func() CalendarChangesCommand {
return CalendarChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
@@ -70,110 +70,8 @@ func (j *Client) GetCalendarChanges(accountId string, sinceState State, maxChang
)
}
type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent]
var _ SearchResults[CalendarEvent] = CalendarEventSearchResults{}
func (r CalendarEventSearchResults) GetResults() []CalendarEvent { return r.Results }
func (r CalendarEventSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges }
func (r CalendarEventSearchResults) GetPosition() uint { return r.Position }
func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit }
func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total }
func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position int, limit uint, calculateTotal bool,
ctx Context) (map[string]CalendarEventSearchResults, SessionState, State, Language, Error) {
return queryN(j, "QueryCalendarEvents", NS_CALENDARS,
[]CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}},
func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit uint) CalendarEventQueryCommand {
return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal}
},
func(accountId string, cmd Command, path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(query CalendarEventQueryResponse, get CalendarEventGetResponse) CalendarEventSearchResults {
return CalendarEventSearchResults{
Results: get.List,
CanCalculateChanges: query.CanCalculateChanges,
Position: query.Position,
Total: uintPtrIf(query.Total, calculateTotal),
Limit: query.Limit,
}
},
accountIds,
filter, sortBy, limit, position, ctx,
)
}
type CalendarEventChanges = ChangesTemplate[CalendarEvent]
// Retrieve the changes in Calendar Events since a given State.
// @api:tags event,changes
func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (CalendarEventChanges, SessionState, State, Language, Error) {
return changes(j, "GetCalendarEventChanges", NS_CALENDARS,
func() CalendarEventChangesCommand {
return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
CalendarEventChangesResponse{},
func(path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarEventChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges {
return CalendarEventChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
return create(j, "CreateCalendarEvent", NS_CALENDARS,
func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ref string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: []string{ref}}
},
func(resp CalendarEventSetResponse) map[string]*CalendarEvent {
return resp.Created
},
func(resp CalendarEventGetResponse) []CalendarEvent {
return resp.List
},
accountId, event,
ctx,
)
}
func (j *Client) DeleteCalendarEvent(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteCalendarEvent", NS_CALENDARS,
func(accountId string, destroy []string) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy}
},
CalendarEventSetResponse{},
accountId, destroyIds,
ctx,
)
}
func (j *Client) CreateCalendar(accountId string, calendar CalendarChange, ctx Context) (*Calendar, SessionState, State, Language, Error) {
return create(j, "CreateCalendar", NS_CALENDARS,
return create(j, "CreateCalendar", CalendarEventType,
func(accountId string, create map[string]CalendarChange) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Create: create}
},
@@ -192,7 +90,7 @@ func (j *Client) CreateCalendar(accountId string, calendar CalendarChange, ctx C
}
func (j *Client) DeleteCalendar(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteCalendar", NS_CALENDARS,
return destroy(j, "DeleteCalendar", CalendarEventType,
func(accountId string, destroy []string) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Destroy: destroy}
},
@@ -203,7 +101,7 @@ func (j *Client) DeleteCalendar(accountId string, destroyIds []string, ctx Conte
}
func (j *Client) UpdateCalendar(accountId string, id string, changes CalendarChange, ctx Context) (Calendar, SessionState, State, Language, Error) {
return update(j, "UpdateCalendar", NS_CALENDARS,
return update(j, "UpdateCalendar", CalendarEventType,
func(update map[string]PatchObject) CalendarSetCommand {
return CalendarSetCommand{AccountId: accountId, Update: update}
},

View File

@@ -3,7 +3,7 @@ package jmap
var NS_CONTACTS = ns(JmapContacts)
func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Context) (ContactCardGetResponse, SessionState, State, Language, Error) {
return get(j, "GetContactCards", NS_CONTACTS,
return get(j, "GetContactCards", ContactCardType,
func(accountId string, ids []string) ContactCardGetCommand {
return ContactCardGetCommand{AccountId: accountId, Ids: contactIds}
},
@@ -19,7 +19,7 @@ type ContactCardChanges = ChangesTemplate[ContactCard]
// Retrieve the changes in Contact Cards since a given State.
// @api:tags contact,changes
func (j *Client) GetContactCardChanges(accountId string, sinceState State, maxChanges uint, ctx Context) (ContactCardChanges, SessionState, State, Language, Error) {
return changes(j, "GetContactCardChanges", NS_CONTACTS,
return changes(j, "GetContactCardChanges", ContactCardType,
func() ContactCardChangesCommand {
return ContactCardChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
@@ -63,7 +63,7 @@ func (j *Client) QueryContactCards(accountIds []string,
filter ContactCardFilterElement, sortBy []ContactCardComparator,
position int, limit uint, calculateTotal bool,
ctx Context) (map[string]ContactCardSearchResults, SessionState, State, Language, Error) {
return queryN(j, "QueryContactCards", NS_CONTACTS,
return queryN(j, "QueryContactCards", ContactCardType,
[]ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}},
func(accountId string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, limit uint) ContactCardQueryCommand {
return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal}
@@ -86,7 +86,7 @@ func (j *Client) QueryContactCards(accountIds []string,
}
func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Context) (*ContactCard, SessionState, State, Language, Error) {
return create(j, "CreateContactCard", NS_CONTACTS,
return create(j, "CreateContactCard", ContactCardType,
func(accountId string, create map[string]ContactCard) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Create: create}
},
@@ -105,7 +105,7 @@ func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Co
}
func (j *Client) DeleteContactCard(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteContactCard", NS_CONTACTS,
return destroy(j, "DeleteContactCard", ContactCardType,
func(accountId string, destroy []string) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Destroy: destroy}
},

View File

@@ -13,13 +13,6 @@ import (
var NS_MAIL = ns(JmapMail)
var NS_MAIL_SUBMISSION = ns(JmapMail, JmapSubmission)
type Emails struct {
Emails []Email `json:"emails,omitempty"`
Total uint `json:"total,omitzero"`
Limit uint `json:"limit,omitzero"`
Offset uint `json:"offset,omitzero"`
}
type getEmailsResult struct {
emails []Email
notFound []string
@@ -32,23 +25,25 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
logger := j.logger("GetEmails", ctx)
ctx = ctx.WithLogger(logger)
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
getEmails := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
if maxBodyValueBytes > 0 {
get.MaxBodyValueBytes = maxBodyValueBytes
getEmails.MaxBodyValueBytes = maxBodyValueBytes
}
invokeGet := invocation(get, "1")
invokeGet := invocation(getEmails, "1")
methodCalls := []Invocation{invokeGet}
var markEmails EmailSetCommand
if markAsSeen {
updates := make(map[string]EmailUpdate, len(ids))
for _, id := range ids {
updates[id] = EmailUpdate{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
}
mark := EmailSetCommand{AccountId: accountId, Update: updates}
methodCalls = []Invocation{invocation(mark, "0"), invokeGet}
markEmails = EmailSetCommand{AccountId: accountId, Update: updates}
methodCalls = []Invocation{invocation(markEmails, "0"), invokeGet}
}
var getThreads ThreadGetRefCommand
if withThreads {
threads := ThreadGetRefCommand{
getThreads = ThreadGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
ResultOf: "1",
@@ -56,7 +51,7 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
Path: "/list/*/" + EmailPropertyThreadId, //NOSONAR
},
}
methodCalls = append(methodCalls, invocation(threads, "2"))
methodCalls = append(methodCalls, invocation(getThreads, "2"))
}
cmd, err := j.request(ctx, NS_MAIL, methodCalls...)
@@ -66,7 +61,7 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
result, sessionState, state, language, gwerr := command(j, ctx, cmd, func(body *Response) (getEmailsResult, State, Error) {
if markAsSeen {
var markResponse EmailSetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandEmailSet, "0", &markResponse)
err = retrieveSet(ctx, body, markEmails, "0", &markResponse)
if err != nil {
return getEmailsResult{}, "", err
}
@@ -76,13 +71,13 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
}
}
var response EmailGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandEmailGet, "1", &response)
err = retrieveGet(ctx, body, getEmails, "1", &response)
if err != nil {
return getEmailsResult{}, "", err
}
if withThreads {
var threads ThreadGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandThreadGet, "2", &threads)
err = retrieveGet(ctx, body, getThreads, "2", &threads)
if err != nil {
return getEmailsResult{}, "", err
}
@@ -116,10 +111,12 @@ func (j *Client) GetEmailBlobId(accountId string, id string, ctx Context) (strin
})
}
type EmailSearchResults = SearchResultsTemplate[Email]
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR
offset int, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool,
ctx Context) (Emails, SessionState, State, Language, Error) {
ctx Context) (EmailSearchResults, SessionState, State, Language, Error) {
logger := j.loggerParams("GetAllEmailsInMailbox", ctx, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Uint(logLimit, limit)
})
@@ -168,36 +165,37 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS
cmd, err := j.request(ctx, NS_MAIL, invocations...)
if err != nil {
return Emails{}, "", "", "", err
return EmailSearchResults{}, "", "", "", err
}
return command(j, ctx, cmd, func(body *Response) (Emails, State, Error) {
return command(j, ctx, cmd, func(body *Response) (EmailSearchResults, State, Error) {
var queryResponse EmailQueryResponse
err = retrieveQuery(ctx, body, query, "0", &queryResponse)
if err != nil {
return Emails{}, "", err
return EmailSearchResults{}, "", err
}
var getResponse EmailGetResponse
err = retrieveGet(ctx, body, get, "1", &getResponse)
if err != nil {
logger.Error().Err(err).Send()
return Emails{}, "", err
return EmailSearchResults{}, "", err
}
if withThreads {
var thread ThreadGetResponse
err = retrieveGet(ctx, body, threads, "2", &thread)
if err != nil {
return Emails{}, "", err
return EmailSearchResults{}, "", err
}
setThreadSize(&thread, getResponse.List)
}
return Emails{
Emails: getResponse.List,
Total: queryResponse.Total,
Limit: queryResponse.Limit,
Offset: queryResponse.Position,
return EmailSearchResults{
Results: getResponse.List,
CanCalculateChanges: queryResponse.CanCalculateChanges,
Position: queryResponse.Position,
Limit: queryResponse.Limit,
Total: uintPtr(queryResponse.Total),
}, queryResponse.QueryState, nil
})
}
@@ -293,17 +291,11 @@ type SearchSnippetWithMeta struct {
SearchSnippet
}
type EmailSnippetQueryResult struct {
Snippets []SearchSnippetWithMeta `json:"snippets,omitempty"`
Total uint `json:"total"`
Limit uint `json:"limit,omitzero"`
Position uint `json:"position,omitzero"`
QueryState State `json:"queryState"`
}
type EmailSnippetSearchResults SearchResultsTemplate[SearchSnippetWithMeta]
func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR
filter EmailFilterElement, offset int, limit uint,
ctx Context) (map[string]EmailSnippetQueryResult, SessionState, State, Language, Error) {
ctx Context) (map[string]EmailSnippetSearchResults, SessionState, State, Language, Error) {
logger := j.loggerParams("QueryEmailSnippets", ctx, func(z zerolog.Context) zerolog.Context {
return z.Uint(logLimit, limit).Int(logOffset, offset)
})
@@ -358,8 +350,9 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR
return nil, "", "", "", err
}
return command(j, ctx, cmd, func(body *Response) (map[string]EmailSnippetQueryResult, State, Error) {
results := make(map[string]EmailSnippetQueryResult, len(uniqueAccountIds))
return command(j, ctx, cmd, func(body *Response) (map[string]EmailSnippetSearchResults, State, Error) {
results := make(map[string]EmailSnippetSearchResults, len(uniqueAccountIds))
states := make(map[string]State, len(uniqueAccountIds))
for _, accountId := range uniqueAccountIds {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
@@ -400,15 +393,17 @@ func (j *Client) QueryEmailSnippets(accountIds []string, //NOSONAR
i++
}
results[accountId] = EmailSnippetQueryResult{
Snippets: snippets,
Total: queryResponse.Total,
Limit: queryResponse.Limit,
Position: queryResponse.Position,
QueryState: queryResponse.QueryState,
states[accountId] = queryResponse.QueryState
results[accountId] = EmailSnippetSearchResults{
Results: snippets,
CanCalculateChanges: queryResponse.CanCalculateChanges,
Total: uintPtr(queryResponse.Total),
Limit: queryResponse.Limit,
Position: queryResponse.Position,
}
}
return results, squashStateFunc(results, func(r EmailSnippetQueryResult) State { return r.QueryState }), nil
return results, squashState(states), nil
})
}
@@ -775,24 +770,15 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
})
}
func (j *Client) DeleteEmails(accountId string, destroy []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
Destroy: destroy,
}
cmd, err := j.request(ctx, NS_MAIL, invocation(set, "0"))
if err != nil {
return nil, "", "", "", err
}
return command(j, ctx, cmd, func(body *Response) (map[string]SetError, State, Error) {
var setResponse EmailSetResponse
err = retrieveSet(ctx, body, set, "0", &setResponse)
if err != nil {
return nil, "", err
}
return setResponse.NotDestroyed, setResponse.NewState, nil
})
func (j *Client) DeleteEmails(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteEmails", EmailType,
func(accountId string, destroy []string) EmailSetCommand {
return EmailSetCommand{AccountId: accountId, Destroy: destroy}
},
EmailSetResponse{},
accountId, destroyIds,
ctx,
)
}
type SubmittedEmail struct {
@@ -1109,7 +1095,7 @@ type EmailSubmissionChanges = ChangesTemplate[EmailSubmission]
// @api:tags email,changes
func (j *Client) GetEmailSubmissionChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (EmailSubmissionChanges, SessionState, State, Language, Error) {
return changes(j, "GetEmailSubmissionChanges", NS_MAIL_SUBMISSION,
return changes(j, "GetEmailSubmissionChanges", EmailSubmissionType,
func() EmailSubmissionChangesCommand {
return EmailSubmissionChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},

105
pkg/jmap/api_event.go Normal file
View File

@@ -0,0 +1,105 @@
package jmap
var NS_CALENDAR_EVENTS = ns(JmapCalendars)
type CalendarEventSearchResults SearchResultsTemplate[CalendarEvent]
var _ SearchResults[CalendarEvent] = CalendarEventSearchResults{}
func (r CalendarEventSearchResults) GetResults() []CalendarEvent { return r.Results }
func (r CalendarEventSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges }
func (r CalendarEventSearchResults) GetPosition() uint { return r.Position }
func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit }
func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total }
func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position int, limit uint, calculateTotal bool,
ctx Context) (map[string]CalendarEventSearchResults, SessionState, State, Language, Error) {
return queryN(j, "QueryCalendarEvents", CalendarEventType,
[]CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}},
func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit uint) CalendarEventQueryCommand {
return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: uintPtr(limit), CalculateTotal: calculateTotal}
},
func(accountId string, cmd Command, path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(query CalendarEventQueryResponse, get CalendarEventGetResponse) CalendarEventSearchResults {
return CalendarEventSearchResults{
Results: get.List,
CanCalculateChanges: query.CanCalculateChanges,
Position: query.Position,
Total: uintPtrIf(query.Total, calculateTotal),
Limit: query.Limit,
}
},
accountIds,
filter, sortBy, limit, position, ctx,
)
}
type CalendarEventChanges = ChangesTemplate[CalendarEvent]
// Retrieve the changes in Calendar Events since a given State.
// @api:tags event,changes
func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (CalendarEventChanges, SessionState, State, Language, Error) {
return changes(j, "GetCalendarEventChanges", CalendarEventType,
func() CalendarEventChangesCommand {
return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
CalendarEventChangesResponse{},
func(path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarEventChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges {
return CalendarEventChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
ctx,
)
}
func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
return create(j, "CreateCalendarEvent", CalendarEventType,
func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ref string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: []string{ref}}
},
func(resp CalendarEventSetResponse) map[string]*CalendarEvent {
return resp.Created
},
func(resp CalendarEventGetResponse) []CalendarEvent {
return resp.List
},
accountId, event,
ctx,
)
}
func (j *Client) DeleteCalendarEvent(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteCalendarEvent", CalendarEventType,
func(accountId string, destroy []string) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy}
},
CalendarEventSetResponse{},
accountId, destroyIds,
ctx,
)
}

View File

@@ -9,7 +9,7 @@ import (
var NS_IDENTITY = ns(JmapMail)
func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
return getA(j, "GetAllIdentities", NS_IDENTITY,
return getA(j, "GetAllIdentities", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
@@ -20,7 +20,7 @@ func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, Se
}
func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
return getA(j, "GetIdentities", NS_IDENTITY,
return getA(j, "GetIdentities", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: ids}
},
@@ -31,7 +31,7 @@ func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Conte
}
func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, ctx Context) (map[string][]Identity, SessionState, State, Language, Error) {
return getN(j, "GetIdentitiesForAllAccounts", NS_IDENTITY,
return getN(j, "GetIdentitiesForAllAccounts", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
@@ -96,7 +96,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [
}
func (j *Client) CreateIdentity(accountId string, identity IdentityChange, ctx Context) (*Identity, SessionState, State, Language, Error) {
return create(j, "CreateIdentity", NS_IDENTITY,
return create(j, "CreateIdentity", IdentityType,
func(accountId string, create map[string]IdentityChange) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Create: create}
},
@@ -115,7 +115,7 @@ func (j *Client) CreateIdentity(accountId string, identity IdentityChange, ctx C
}
func (j *Client) UpdateIdentity(accountId string, id string, changes IdentityChange, ctx Context) (Identity, SessionState, State, Language, Error) {
return update(j, "UpdateIdentity", NS_IDENTITY,
return update(j, "UpdateIdentity", IdentityType,
func(update map[string]PatchObject) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Update: update}
},
@@ -130,7 +130,7 @@ func (j *Client) UpdateIdentity(accountId string, id string, changes IdentityCha
}
func (j *Client) DeleteIdentity(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteIdentity", NS_IDENTITY,
return destroy(j, "DeleteIdentity", IdentityType,
func(accountId string, destroy []string) IdentitySetCommand {
return IdentitySetCommand{AccountId: accountId, Destroy: destroyIds}
},
@@ -146,7 +146,7 @@ type IdentityChanges = ChangesTemplate[Identity]
// @api:tags email,changes
func (j *Client) GetIdentityChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (IdentityChanges, SessionState, State, Language, Error) {
return changes(j, "GetIdentityChanges", NS_IDENTITY,
return changes(j, "GetIdentityChanges", IdentityType,
func() IdentityChangesCommand {
return IdentityChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},

View File

@@ -9,32 +9,27 @@ import (
var NS_MAILBOX = ns(JmapMail)
func (j *Client) GetMailbox(accountId string, ids []string, ctx Context) (MailboxGetResponse, SessionState, State, Language, Error) {
/*
return get(j, "GetMailbox", NS_MAILBOX,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: ids}
},
MailboxGetResponse{},
identity1,
accountId, session, ctx, logger, acceptLanguage, ids,
)
*/
return fget[Mailboxes](MAILBOX, j, "GetMailbox", accountId, ids, ctx)
return get(j, "GetMailbox", MailboxType,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: ids}
},
MailboxGetResponse{},
identity1,
accountId, ids,
ctx,
)
}
func (j *Client) GetAllMailboxes(accountIds []string, ctx Context) (map[string][]Mailbox, SessionState, State, Language, Error) {
/*
return getAN(j, "GetAllMailboxes", NS_MAILBOX,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId}
},
MailboxGetResponse{},
identity1,
accountIds, session, ctx, logger, acceptLanguage, []string{},
)
*/
return fgetAN[Mailboxes](MAILBOX, j, "GetAllMailboxes", identity1, accountIds, []string{}, ctx)
return getAN(j, "GetAllMailboxes", MailboxType,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId}
},
MailboxGetResponse{},
identity1,
accountIds, []string{},
ctx,
)
}
func (j *Client) SearchMailboxes(accountIds []string, filter MailboxFilterElement, ctx Context) (map[string][]Mailbox, SessionState, State, Language, Error) {
@@ -135,7 +130,7 @@ func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, u
// @apidoc mailboxes,changes
func (j *Client) GetMailboxChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (MailboxChanges, SessionState, State, Language, Error) {
return changesA(j, "GetMailboxChanges", NS_MAILBOX,
return changesA(j, "GetMailboxChanges", MailboxType,
func() MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
@@ -161,7 +156,7 @@ func (j *Client) GetMailboxChanges(accountId string, sinceState State, maxChange
func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOSONAR
sinceStateMap map[string]State, maxChanges uint,
ctx Context) (map[string]MailboxChanges, SessionState, State, Language, Error) {
return changesN(j, "GetMailboxChangesForMultipleAccounts", NS_MAILBOX,
return changesN(j, "GetMailboxChangesForMultipleAccounts", MailboxType,
accountIds, sinceStateMap,
func(accountId string, state State) MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: state, MaxChanges: uintPtr(maxChanges)}
@@ -179,59 +174,22 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOS
}
func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Context) (map[string][]string, SessionState, State, Language, Error) {
logger := j.logger("GetMailboxRolesForMultipleAccounts", ctx)
ctx = ctx.WithLogger(logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return nil, "", "", "", nil
}
t := true
invocations := make([]Invocation, n*2)
for i, accountId := range uniqueAccountIds {
invocations[i*2+0] = invocation(MailboxQueryCommand{
AccountId: accountId,
Filter: MailboxFilterCondition{
HasAnyRole: &t,
},
}, mcid(accountId, "0"))
invocations[i*2+1] = invocation(MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
ResultOf: mcid(accountId, "0"),
Name: CommandMailboxQuery,
Path: "/ids",
},
}, mcid(accountId, "1"))
}
cmd, err := j.request(ctx, NS_MAILBOX, invocations...)
if err != nil {
return nil, "", "", "", err
}
return command(j, ctx, cmd, func(body *Response) (map[string][]string, State, Error) {
resp := make(map[string][]string, n)
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var getResponse MailboxGetResponse
err = retrieveResponseMatchParameters(ctx, body, CommandMailboxGet, mcid(accountId, "1"), &getResponse)
if err != nil {
return nil, "", err
}
roles := make([]string, len(getResponse.List))
for i, mailbox := range getResponse.List {
roles[i] = mailbox.Role
}
return queryN(j, "GetMailboxRolesForMultipleAccounts", MailboxType,
[]MailboxComparator{{Property: MailboxPropertySortOrder, IsAscending: true}},
func(accountId string, filter MailboxFilterCondition, sortBy []MailboxComparator, _ int, _ uint) MailboxQueryCommand {
return MailboxQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, SortAsTree: false, FilterAsTree: false, Position: 0, Limit: nil, CalculateTotal: false}
},
func(accountId string, cmd Command, path, rof string) MailboxGetRefCommand {
return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
},
func(_ MailboxQueryResponse, get MailboxGetResponse) []string {
roles := structs.Map(get.List, func(m Mailbox) string { return m.Role })
slices.Sort(roles)
resp[accountId] = roles
stateByAccountId[accountId] = getResponse.State
}
return resp, squashState(stateByAccountId), nil
})
return roles
},
accountIds, MailboxFilterCondition{HasAnyRole: boolPtr(true)}, nil, 0, 0,
ctx,
)
}
func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, ctx Context) (map[string]string, SessionState, State, Language, Error) {
@@ -286,7 +244,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, ctx Contex
func (j *Client) UpdateMailbox(accountId string, mailboxId string, change MailboxChange, //NOSONAR
ctx Context) (Mailbox, SessionState, State, Language, Error) {
return update(j, "UpdateMailbox", NS_MAILBOX,
return update(j, "UpdateMailbox", MailboxType,
func(update map[string]PatchObject) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Update: update}
},
@@ -301,7 +259,7 @@ func (j *Client) UpdateMailbox(accountId string, mailboxId string, change Mailbo
}
func (j *Client) CreateMailbox(accountId string, mailbox MailboxChange, ctx Context) (*Mailbox, SessionState, State, Language, Error) {
return create(j, "CreateMailbox", NS_MAILBOX,
return create(j, "CreateMailbox", MailboxType,
func(accountId string, create map[string]MailboxChange) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Create: create}
},
@@ -320,7 +278,7 @@ func (j *Client) CreateMailbox(accountId string, mailbox MailboxChange, ctx Cont
}
func (j *Client) DeleteMailboxes(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteMailboxes", NS_MAILBOX,
return destroy(j, "DeleteMailboxes", MailboxType,
func(accountId string, destroy []string) MailboxSetCommand {
return MailboxSetCommand{AccountId: accountId, Destroy: destroyIds}
},

View File

@@ -3,7 +3,7 @@ package jmap
var NS_PRINCIPALS = ns(JmapPrincipals)
func (j *Client) GetPrincipals(accountId string, ids []string, ctx Context) (PrincipalGetResponse, SessionState, State, Language, Error) {
return get(j, "GetPrincipals", NS_PRINCIPALS,
return get(j, "GetPrincipals", PrincipalType,
func(accountId string, ids []string) PrincipalGetCommand {
return PrincipalGetCommand{AccountId: accountId, Ids: ids}
},
@@ -28,7 +28,7 @@ func (j *Client) QueryPrincipals(accountId string,
filter PrincipalFilterElement, sortBy []PrincipalComparator,
position uint, limit uint, calculateTotal bool,
ctx Context) (PrincipalSearchResults, SessionState, State, Language, Error) {
return query(j, "QueryPrincipals", NS_PRINCIPALS,
return query(j, "QueryPrincipals", PrincipalType,
[]PrincipalComparator{{Property: PrincipalPropertyName, IsAscending: true}},
func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit uint) PrincipalQueryCommand {
return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal}

View File

@@ -3,7 +3,7 @@ package jmap
var NS_QUOTA = ns(JmapQuota)
func (j *Client) GetQuotas(accountIds []string, ctx Context) (map[string]QuotaGetResponse, SessionState, State, Language, Error) {
return getN(j, "GetQuotas", NS_QUOTA,
return getN(j, "GetQuotas", QuotaType,
func(accountId string, ids []string) QuotaGetCommand {
return QuotaGetCommand{AccountId: accountId}
},
@@ -21,7 +21,7 @@ type QuotaChanges = ChangesTemplate[Quota]
// @api:tags quota,changes
func (j *Client) GetQuotaChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (QuotaChanges, SessionState, State, Language, Error) {
return changesA(j, "GetQuotaChanges", NS_QUOTA,
return changesA(j, "GetQuotaChanges", QuotaType,
func() QuotaChangesCommand {
return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},
@@ -53,7 +53,7 @@ func (j *Client) GetQuotaChanges(accountId string, sinceState State, maxChanges
func (j *Client) GetQuotaUsageChanges(accountId string, sinceState State, maxChanges uint,
ctx Context) (QuotaChanges, SessionState, State, Language, Error) {
return updates(j, "GetQuotaUsageChanges", NS_QUOTA,
return updates(j, "GetQuotaUsageChanges", QuotaType,
func() QuotaChangesCommand {
return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
},

View File

@@ -12,7 +12,7 @@ const (
)
func (j *Client) GetVacationResponse(accountId string, ctx Context) (VacationResponseGetResponse, SessionState, State, Language, Error) {
return get(j, "GetVacationResponse", NS_VACATION,
return get(j, "GetVacationResponse", VacationResponseType,
func(accountId string, ids []string) VacationResponseGetCommand {
return VacationResponseGetCommand{AccountId: accountId}
},

View File

@@ -6,12 +6,35 @@ import (
"fmt"
"go/ast"
"go/token"
"iter"
"log"
"strings"
"golang.org/x/tools/go/packages"
)
func valuesOf(p *packages.Package) iter.Seq[*ast.ValueSpec] { //NOSONAR
return func(yield func(*ast.ValueSpec) bool) {
for _, syn := range p.Syntax {
for _, decl := range syn.Decls {
g, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
for _, s := range g.Specs {
e, ok := s.(*ast.ValueSpec)
if !ok {
continue
}
if !yield(e) {
return
}
}
}
}
}
}
func parseConsts(pkgID string, suffix string, typeName string) (map[string]string, error) { //NOSONAR
result := map[string]string{}
{
@@ -31,29 +54,19 @@ func parseConsts(pkgID string, suffix string, typeName string) (map[string]strin
if p.ID != pkgID {
continue
}
for _, syn := range p.Syntax {
for _, decl := range syn.Decls {
switch g := decl.(type) {
case *ast.GenDecl:
for _, s := range g.Specs {
switch e := s.(type) {
case *ast.ValueSpec:
for i, ident := range e.Names {
if ident != nil && strings.HasSuffix(ident.Name, suffix) {
value := e.Values[i]
switch c := value.(type) {
case *ast.CallExpr:
switch f := c.Fun.(type) {
case *ast.Ident:
if f.Name == typeName {
switch a := c.Args[0].(type) {
case *ast.BasicLit:
if a.Kind == token.STRING {
result[ident.Name] = strings.Trim(a.Value, `"`)
}
}
}
}
for v := range valuesOf(p) {
for i, ident := range v.Names {
if ident != nil && strings.HasSuffix(ident.Name, suffix) {
value := v.Values[i]
switch c := value.(type) {
case *ast.CallExpr:
switch f := c.Fun.(type) {
case *ast.Ident:
if f.Name == typeName {
switch a := c.Args[0].(type) {
case *ast.BasicLit:
if a.Kind == token.STRING {
result[ident.Name] = strings.Trim(a.Value, `"`)
}
}
}

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -577,7 +578,7 @@ type HttpWsClient struct {
func (w *HttpWsClient) readPump() { //NOSONAR
logger := log.From(w.logger.With().Str("username", w.username))
defer func() {
if err := w.c.Close(); err != nil {
if err := w.c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
logger.Warn().Err(err).Msg("failed to close websocket connection")
}
}()

View File

@@ -126,7 +126,7 @@ func TestEvents(t *testing.T) {
results := m[accountId]
require.Equal(len(results.Results), int(page))
require.Equal(limit, results.Limit)
require.Equal(position, results.Position)
require.Equal(uint(position), results.Position)
require.Equal(true, results.CanCalculateChanges)
require.NotNil(results.Total)
require.Equal(count, *results.Total)

View File

@@ -107,7 +107,8 @@ func TestContacts(t *testing.T) {
require.Len(results.Results, int(count))
require.Equal(uint(0), results.Limit)
require.Equal(uint(0), results.Position)
require.Equal(uint(0), results.Total)
require.NotNil(results.Total)
require.Equal(count, *results.Total)
require.Equal(true, results.CanCalculateChanges)
for _, actual := range results.Results {

View File

@@ -85,8 +85,8 @@ func TestEmails(t *testing.T) {
require.NoError(err)
require.Equal(session.State, sessionState)
require.Equalf(threads, len(resp.Emails), "the number of collapsed emails in the inbox is expected to be %v, but is actually %v", threads, len(resp.Emails))
for _, e := range resp.Emails {
require.Equalf(threads, len(resp.Results), "the number of collapsed emails in the inbox is expected to be %v, but is actually %v", threads, len(resp.Results))
for _, e := range resp.Results {
require.Len(e.MessageId, 1)
expectation, ok := mailsByMessageId[e.MessageId[0]]
require.True(ok)
@@ -99,8 +99,8 @@ func TestEmails(t *testing.T) {
require.NoError(err)
require.Equal(session.State, sessionState)
require.Equalf(count, len(resp.Emails), "the number of emails in the inbox is expected to be %v, but is actually %v", count, len(resp.Emails))
for _, e := range resp.Emails {
require.Equalf(count, len(resp.Results), "the number of emails in the inbox is expected to be %v, but is actually %v", count, len(resp.Results))
for _, e := range resp.Results {
require.Len(e.MessageId, 1)
expectation, ok := mailsByMessageId[e.MessageId[0]]
require.True(ok)
@@ -168,7 +168,13 @@ func TestSendingEmails(t *testing.T) {
accountId string
session *Session
}{{toAccountId, toSession}, {ccAccountId, ccSession}} {
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{u.accountId}, ctx)
uctx := Context{
Session: u.session,
Context: ctx.Context,
Logger: ctx.Logger,
AcceptLanguage: ctx.AcceptLanguage,
}
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{u.accountId}, uctx)
require.NoError(err)
for _, mailbox := range mailboxes[u.accountId] {
require.Equal(0, mailbox.TotalEmails)
@@ -288,7 +294,13 @@ func TestSendingEmails(t *testing.T) {
accountId string
session *Session
}{{to, toAccountId, toSession}, {cc, ccAccountId, ccSession}} {
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{r.accountId}, ctx)
rctx := Context{
Session: r.session,
Context: ctx.Context,
Logger: ctx.Logger,
AcceptLanguage: ctx.AcceptLanguage,
}
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{r.accountId}, rctx)
require.NoError(err)
inboxId := ""
for _, mailbox := range mailboxes[r.accountId] {
@@ -299,7 +311,7 @@ func TestSendingEmails(t *testing.T) {
}
require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.name)
emails, _, _, _, err := s.client.QueryEmails([]string{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, ctx)
emails, _, _, _, err := s.client.QueryEmails([]string{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, rctx)
require.NoError(err)
require.Contains(emails, r.accountId)
require.Len(emails[r.accountId].Emails, 1)

View File

@@ -1,6 +1,7 @@
package jmap
import (
"slices"
"sync"
"sync/atomic"
"testing"
@@ -223,7 +224,15 @@ func TestWs(t *testing.T) {
require.Equal(state, changes.NewState)
require.Empty(changes.Created)
require.Len(changes.Destroyed, 2)
require.EqualValues(emailIds, changes.Destroyed)
{
a := make([]string, len(emailIds))
copy(a, emailIds)
slices.Sort(emailIds)
b := make([]string, len(changes.Destroyed))
copy(b, changes.Destroyed)
slices.Sort(changes.Destroyed)
require.EqualValues(a, b)
}
require.Empty(changes.Updated)
lastState = state
}

View File

@@ -1564,6 +1564,34 @@ var _ Idable = &Mailbox{}
func (f Mailbox) GetObjectType() ObjectType { return MailboxType }
func (f Mailbox) GetId() string { return f.Id }
const (
MailboxPropertyId = "id"
MailboxPropertyName = "name"
MailboxPropertyParentId = "parentId"
MailboxPropertyRole = "role"
MailboxPropertySortOrder = "sortOrder"
MailboxPropertyTotalEmails = "totalEmails"
MailboxPropertyUnreadEmails = "unreadEmails"
MailboxPropertyTotalThreads = "totalThreads"
MailboxPropertyUnreadThreads = "unreadThreads"
MailboxPropertyMyRights = "myRights"
MailboxPropertyIsSubscribed = "isSubscribed"
)
var MailboxProperties = []string{
MailboxPropertyId,
MailboxPropertyName,
MailboxPropertyParentId,
MailboxPropertyRole,
MailboxPropertySortOrder,
MailboxPropertyTotalEmails,
MailboxPropertyUnreadEmails,
MailboxPropertyTotalThreads,
MailboxPropertyUnreadThreads,
MailboxPropertyMyRights,
MailboxPropertyIsSubscribed,
}
type MailboxChange struct {
// User-visible name for the Mailbox, e.g., “Inbox”.
//
@@ -1750,6 +1778,49 @@ type MailboxQueryCommand struct {
Sort []MailboxComparator `json:"sort,omitempty"`
SortAsTree bool `json:"sortAsTree,omitempty"`
FilterAsTree bool `json:"filterAsTree,omitempty"`
// The zero-based index of the first id in the full list of results to return.
//
// If a negative value is given, it is an offset from the end of the list.
// Specifically, the negative value MUST be added to the total number of results given
// the filter, and if still negative, its clamped to 0. This is now the zero-based
// index of the first id to return.
//
// If the index is greater than or equal to the total number of objects in the results
// list, then the ids array in the response will be empty, but this is not an error.
Position int `json:"position,omitempty"`
// An Email id.
//
// If supplied, the position argument is ignored.
// The index of this id in the results will be used in combination with the anchorOffset
// argument to determine the index of the first result to return.
Anchor string `json:"anchor,omitempty"`
// The index of the first result to return relative to the index of the anchor,
// if an anchor is given.
//
// This MAY be negative.
//
// For example, -1 means the object immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" doc:"opt" default:"0"`
// The maximum number of results to return.
//
// If null, no limit presumed.
// The server MAY choose to enforce a maximum limit argument.
// In this case, if a greater value is given (or if it is null), the limit is clamped
// to the maximum; the new limit is returned with the response so the client is aware.
//
// If a negative value is given, the call MUST be rejected with an invalidArguments error.
Limit *uint `json:"limit,omitempty"`
// Does the client wish to know the total number of results in the query?
//
// This may be slow and expensive for servers to calculate, particularly with complex filters,
// so clients should take care to only request the total when needed.
CalculateTotal bool `json:"calculateTotal,omitempty"`
}
var _ QueryCommand[Mailbox] = &MailboxQueryCommand{}
@@ -3673,6 +3744,7 @@ var _ GetResponse[Thread] = &ThreadGetResponse{}
func (r ThreadGetResponse) GetState() State { return r.State }
func (r ThreadGetResponse) GetNotFound() []string { return r.NotFound }
func (r ThreadGetResponse) GetList() []Thread { return r.List }
func (r ThreadGetResponse) GetMarker() Thread { return Thread{} }
type IdentityGetCommand struct {
AccountId string `json:"accountId"`

View File

@@ -806,12 +806,13 @@ func (e Exemplar) EmailBodyPart() EmailBodyPart {
}
}
func (e Exemplar) Emails() Emails {
return Emails{
Emails: []Email{e.Email()},
Total: 132,
Limit: 1,
Offset: 5,
func (e Exemplar) Emails() EmailSearchResults {
return EmailSearchResults{
Results: []Email{e.Email()},
Total: uintPtr(132),
Limit: 1,
Position: 5,
CanCalculateChanges: true,
}
}
@@ -820,7 +821,7 @@ func (e Exemplar) EmailGetResponse() EmailGetResponse {
AccountId: e.AccountId,
State: "aesh2ahj",
NotFound: []string{"ahx"},
List: e.Emails().Emails,
List: e.Emails().Results,
}
}

View File

@@ -8,72 +8,8 @@ import (
"github.com/rs/zerolog"
)
type Factory[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any] interface {
Namespaces() []JmapNamespace
CreateGetCommand(accountId string, ids []string) GETREQ
CreateGetResponse() GETRESP
MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []T, destroyed []string) CHANGES
}
type Mailboxes string
const MAILBOX = Mailboxes("MAILBOX")
var _ Factory[Mailbox, MailboxGetCommand, MailboxGetResponse, MailboxChanges] = MAILBOX
func (f Mailboxes) Namespaces() []JmapNamespace {
return NS_MAILBOX
}
func (f Mailboxes) CreateGetCommand(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: ids}
}
func (f Mailboxes) CreateGetResponse() MailboxGetResponse {
return MailboxGetResponse{}
}
func (f Mailboxes) MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
return MailboxChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
}
func fget[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
client *Client, name string,
accountId string, ids []string,
ctx Context) (GETRESP, SessionState, State, Language, Error) {
var getresp GETRESP
return get(client, name, f.Namespaces(),
f.CreateGetCommand,
getresp,
identity1,
accountId, ids,
ctx,
)
}
func fgetA[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
client *Client, name string,
accountId string, ids []string,
ctx Context) ([]T, SessionState, State, Language, Error) {
var getresp GETRESP
return getA(client, name, f.Namespaces(),
f.CreateGetCommand,
getresp,
accountId,
ids,
ctx,
)
}
func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
_ GETRESP,
mapper func(GETRESP) RESP,
@@ -83,7 +19,7 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSON
var zero RESP
get := getCommandFactory(accountId, ids)
cmd, err := client.request(ctx, using, invocation(get, "0"))
cmd, err := client.request(ctx, objType.Namespaces, invocation(get, "0"))
if err != nil {
return zero, "", "", "", err
}
@@ -100,35 +36,20 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSON
}
func getA[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T]]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
resp GETRESP,
accountId string, ids []string, ctx Context) ([]T, SessionState, State, Language, Error) {
return get(client, name, using, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, ids, ctx)
}
func fgetAN[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any, CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
client *Client, name string,
respMapper func(map[string][]T) RESP,
accountIds []string, ids []string,
ctx Context) (RESP, SessionState, State, Language, Error) {
var getresp GETRESP
return getAN(client, name, f.Namespaces(),
f.CreateGetCommand,
getresp,
respMapper,
accountIds, ids,
ctx,
)
return get(client, name, objType, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, ids, ctx)
}
func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
resp GETRESP,
respMapper func(map[string][]T) RESP,
accountIds []string, ids []string, ctx Context) (RESP, SessionState, State, Language, Error) {
return getN(client, name, using, getCommandFactory, resp,
return getN(client, name, objType, getCommandFactory, resp,
func(r GETRESP) []T { return r.GetList() },
respMapper,
accountIds, ids,
@@ -137,7 +58,7 @@ func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOS
}
func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
_ GETRESP,
itemMapper func(GETRESP) ITEM,
@@ -158,7 +79,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP an
invocations[i] = invocation(get, mcid(accountId, "0"))
}
cmd, err := client.request(ctx, using, invocations...)
cmd, err := client.request(ctx, objType.Namespaces, invocations...)
if err != nil {
return zero, "", "", "", err
}
@@ -180,7 +101,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP an
}
func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
setCommandFactory func(string, map[string]C) SETREQ,
getCommandFactory func(string, string) GETREQ,
createdMapper func(SETRESP) map[string]*T,
@@ -193,7 +114,7 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se
createMap := map[string]C{"c": create}
get := getCommandFactory(accountId, "#c")
set := setCommandFactory(accountId, createMap)
cmd, err := client.request(ctx, using,
cmd, err := client.request(ctx, objType.Namespaces,
invocation(set, "0"),
invocation(get, "1"),
)
@@ -240,14 +161,14 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se
})
}
func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR
func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, objType ObjectType, //NOSONAR
setCommandFactory func(string, []string) REQ, _ RESP,
accountId string, destroy []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
logger := client.logger(name, ctx)
ctx = ctx.WithLogger(logger)
set := setCommandFactory(accountId, destroy)
cmd, err := client.request(ctx, using,
cmd, err := client.request(ctx, objType.Namespaces,
invocation(set, "0"),
)
if err != nil {
@@ -265,7 +186,7 @@ func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name
}
func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
changesCommandFactory func() CHANGESREQ,
changesResp CHANGESRESP,
_ GETRESP,
@@ -273,7 +194,7 @@ func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
respMapper func(State, State, bool, []T, []T, []string) RESP,
ctx Context) (RESP, SessionState, State, Language, Error) {
return changes(client, name, using, changesCommandFactory, changesResp, getCommandFactory,
return changes(client, name, objType, changesCommandFactory, changesResp, getCommandFactory,
func(r GETRESP) []T { return r.GetList() },
respMapper,
ctx,
@@ -281,7 +202,7 @@ func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
}
func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
changesCommandFactory func() CHANGESREQ,
_ CHANGESRESP,
getCommandFactory func(string, string) GETREQ,
@@ -295,7 +216,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
getCreated := getCommandFactory("/created", "0") //NOSONAR
getUpdated := getCommandFactory("/updated", "0") //NOSONAR
cmd, err := client.request(ctx.WithLogger(logger), using,
cmd, err := client.request(ctx.WithLogger(logger), objType.Namespaces,
invocation(changes, "0"),
invocation(getCreated, "1"),
invocation(getUpdated, "2"),
@@ -335,7 +256,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
}
func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, CHANGESITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
accountIds []string, sinceStateMap map[string]State,
changesCommandFactory func(string, State) CHANGESREQ,
_ CHANGESRESP,
@@ -362,8 +283,9 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
}
invocations := make([]Invocation, n*3)
getCommand := Command("")
changesCommand := Command("")
var ch CHANGESREQ
var gc GETREQ
var gu GETREQ
for i, accountId := range uniqueAccountIds {
sinceState, ok := sinceStateMap[accountId]
if !ok {
@@ -379,13 +301,14 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
invocations[i*3+1] = invocation(getCreated, mcid(accountId, "1"))
invocations[i*3+2] = invocation(getUpdated, mcid(accountId, "2"))
changesCommand = changes.GetCommand()
getCommand = getCreated.GetCommand()
ch = changes
gc = getCreated
gu = getUpdated
}
ctx = ctx.WithLogger(logger)
cmd, err := client.request(ctx, using, invocations...)
cmd, err := client.request(ctx, objType.Namespaces, invocations...)
if err != nil {
return zero, "", "", "", err
}
@@ -395,19 +318,19 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(ctx, body, changesCommand, mcid(accountId, "0"), &changesResponse)
err = retrieveChanges(ctx, body, ch, mcid(accountId, "0"), &changesResponse)
if err != nil {
return zero, "", err
}
var createdResponse GETRESP
err = retrieveResponseMatchParameters(ctx, body, getCommand, mcid(accountId, "1"), &createdResponse)
err = retrieveGet(ctx, body, gc, mcid(accountId, "1"), &createdResponse)
if err != nil {
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(ctx, body, getCommand, mcid(accountId, "2"), &updatedResponse)
err = retrieveGet(ctx, body, gu, mcid(accountId, "2"), &updatedResponse)
if err != nil {
return zero, "", err
}
@@ -422,7 +345,7 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES
}
func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
changesCommandFactory func() CHANGESREQ,
_ CHANGESRESP,
getCommandFactory func(string, string) GETREQ,
@@ -435,7 +358,7 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
changes := changesCommandFactory()
getUpdated := getCommandFactory("/updated", "0") //NOSONAR
cmd, err := client.request(ctx, using,
cmd, err := client.request(ctx, objType.Namespaces,
invocation(changes, "0"),
invocation(getUpdated, "1"),
)
@@ -464,7 +387,8 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
})
}
func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR
func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR
client *Client, name string, objType ObjectType,
setCommandFactory func(map[string]PatchObject) SET,
getCommandFactory func(string) GET,
notUpdatedExtractor func(SETRESP) map[string]SetError,
@@ -473,9 +397,10 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an
ctx Context) (RESP, SessionState, State, Language, Error) {
logger := client.logger(name, ctx)
ctx = ctx.WithLogger(logger)
update := setCommandFactory(map[string]PatchObject{id: changes.AsPatch()})
get := getCommandFactory(id)
cmd, err := client.request(ctx, using, invocation(update, "0"), invocation(get, "1"))
cmd, err := client.request(ctx, objType.Namespaces, invocation(update, "0"), invocation(get, "1"))
var zero RESP
if err != nil {
return zero, "", "", "", err
@@ -503,7 +428,7 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an
}
func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
defaultSortBy []SORT,
queryCommandFactory func(filter FILTER, sortBy []SORT, limit uint, position uint) QUERY,
getCommandFactory func(cmd Command, path string, rof string) GET,
@@ -523,7 +448,7 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T]
var zero RESP
cmd, err := client.request(ctx, using, invocation(query, "0"), invocation(get, "1"))
cmd, err := client.request(ctx, objType.Namespaces, invocation(query, "0"), invocation(get, "1"))
if err != nil {
return zero, "", "", "", err
}
@@ -544,7 +469,7 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T]
}
func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
client *Client, name string, objType ObjectType,
defaultSortBy []SORT,
queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, limit uint) QUERY,
getCommandFactory func(accountId string, cmd Command, path string, rof string) GET,
@@ -552,6 +477,9 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T
accountIds []string,
filter FILTER, sortBy []SORT, limit uint, position int,
ctx Context) (map[string]RESP, SessionState, State, Language, Error) {
logger := client.logger(name, ctx)
ctx = ctx.WithLogger(logger)
uniqueAccountIds := structs.Uniq(accountIds)
if sortBy == nil {
@@ -570,7 +498,7 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T
g = get
}
cmd, err := client.request(ctx, NS_CALENDARS, invocations...)
cmd, err := client.request(ctx, objType.Namespaces, invocations...)
if err != nil {
return nil, "", "", "", err
}

View File

@@ -109,16 +109,17 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
return req.jmapError(accountId, jerr, sessionState, lang)
}
sanitized, err := req.sanitizeEmails(emails.Emails)
sanitized, err := req.sanitizeEmails(emails.Results)
if err != nil {
return req.error(accountId, err)
}
safe := jmap.Emails{
Emails: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Offset: emails.Offset,
safe := jmap.EmailSearchResults{
Results: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Position: emails.Position,
CanCalculateChanges: emails.CanCalculateChanges,
}
return req.respond(accountId, safe, sessionState, EmailResponseObjectType, state)
@@ -689,15 +690,17 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
var totalOverAllAccounts uint = 0
total := 0
for _, results := range resultsByAccountId {
totalOverAllAccounts += results.Total
total += len(results.Snippets)
if results.Total != nil {
totalOverAllAccounts += *results.Total
}
total += len(results.Results)
}
flattened := make([]Snippet, total)
{
i := 0
for accountId, results := range resultsByAccountId {
for _, result := range results.Snippets {
for _, result := range results.Results {
flattened[i] = Snippet{
AccountId: accountId,
SearchSnippetWithMeta: result,