diff --git a/go.mod b/go.mod index 755937cd3b..5573417a86 100644 --- a/go.mod +++ b/go.mod @@ -122,6 +122,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/term v0.41.0 golang.org/x/text v0.35.0 + golang.org/x/tools v0.42.0 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 @@ -405,7 +406,6 @@ require ( golang.org/x/mod v0.33.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect diff --git a/pkg/jmap/api_addressbook.go b/pkg/jmap/api_addressbook.go index 650f5d41d1..bcd55ecaa4 100644 --- a/pkg/jmap/api_addressbook.go +++ b/pkg/jmap/api_addressbook.go @@ -8,41 +8,28 @@ import ( var NS_ADDRESSBOOKS = ns(JmapContacts) -type AddressBooksResponse struct { - AddressBooks []AddressBook `json:"addressbooks"` - NotFound []string `json:"notFound,omitempty"` -} - -func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, State, Language, Error) { +func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBookGetResponse, SessionState, State, Language, Error) { return get(j, "GetAddressbooks", NS_ADDRESSBOOKS, func(accountId string, ids []string) AddressBookGetCommand { return AddressBookGetCommand{AccountId: accountId, Ids: ids} }, AddressBookGetResponse{}, - func(resp AddressBookGetResponse) AddressBooksResponse { - return AddressBooksResponse{AddressBooks: resp.List, NotFound: resp.NotFound} - }, + identity1, accountId, session, ctx, logger, acceptLanguage, ids, ) } -type AddressBookChanges struct { - HasMoreChanges bool `json:"hasMoreChanges"` - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - Created []AddressBook `json:"created,omitempty"` - Updated []AddressBook `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type AddressBookChanges = ChangesTemplate[AddressBook] // Retrieve Address Book changes since a given state. // @apidoc addressbook,changes func (j *Client) GetAddressbookChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (AddressBookChanges, SessionState, State, Language, Error) { - return changes(j, "GetAddressbookChanges", NS_ADDRESSBOOKS, + return changesA(j, "GetAddressbookChanges", NS_ADDRESSBOOKS, func() AddressBookChangesCommand { return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} }, AddressBookChangesResponse{}, + AddressBookGetResponse{}, func(path string, rof string) AddressBookGetRefCommand { return AddressBookGetRefCommand{ AccountId: accountId, @@ -53,7 +40,6 @@ func (j *Client) GetAddressbookChanges(accountId string, session *Session, ctx c }, } }, - func(resp AddressBookGetResponse) []AddressBook { return resp.List }, func(oldState, newState State, hasMoreChanges bool, created, updated []AddressBook, destroyed []string) AddressBookChanges { return AddressBookChanges{ OldState: oldState, diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 9f52663345..bb5e49c539 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -29,32 +29,18 @@ func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx cont }) } -type CalendarsResponse struct { - Calendars []Calendar `json:"calendars"` - NotFound []string `json:"notFound,omitempty"` -} - -func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarsResponse, SessionState, State, Language, Error) { +func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarGetResponse, SessionState, State, Language, Error) { return get(j, "GetCalendars", NS_CALENDARS, func(accountId string, ids []string) CalendarGetCommand { return CalendarGetCommand{AccountId: accountId, Ids: ids} }, CalendarGetResponse{}, - func(resp CalendarGetResponse) CalendarsResponse { - return CalendarsResponse{Calendars: resp.List, NotFound: resp.NotFound} - }, + identity1, accountId, session, ctx, logger, acceptLanguage, ids, ) } -type CalendarChanges struct { - HasMoreChanges bool `json:"hasMoreChanges"` - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - Created []Calendar `json:"created,omitempty"` - Updated []Calendar `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type CalendarChanges = ChangesTemplate[Calendar] // Retrieve Calendar changes since a given state. // @apidoc calendar,changes @@ -148,14 +134,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx }) } -type CalendarEventChanges struct { - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - HasMoreChanges bool `json:"hasMoreChanges"` - Created []CalendarEvent `json:"created,omitempty"` - Updated []CalendarEvent `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type CalendarEventChanges = ChangesTemplate[CalendarEvent] // Retrieve the changes in Calendar Events since a given State. // @api:tags event,changes @@ -236,7 +215,7 @@ func (j *Client) CreateCalendar(accountId string, session *Session, ctx context. } func (j *Client) DeleteCalendar(accountId string, destroyIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { - return destroy(j, "DeleteCalendar", NS_ADDRESSBOOKS, + return destroy(j, "DeleteCalendar", NS_CALENDARS, func(accountId string, destroy []string) CalendarSetCommand { return CalendarSetCommand{AccountId: accountId, Destroy: destroy} }, @@ -244,3 +223,17 @@ func (j *Client) DeleteCalendar(accountId string, destroyIds []string, session * accountId, destroyIds, session, ctx, logger, acceptLanguage, ) } + +func (j *Client) UpdateCalendar(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, changes CalendarChange) (Calendar, SessionState, State, Language, Error) { + return update(j, "UpdateCalendar", NS_CALENDARS, + func(update map[string]PatchObject) CalendarSetCommand { + return CalendarSetCommand{AccountId: accountId, Update: update} + }, + func(id string) CalendarGetCommand { + return CalendarGetCommand{AccountId: accountId, Ids: []string{id}} + }, + func(resp CalendarSetResponse) map[string]SetError { return resp.NotUpdated }, + func(resp CalendarGetResponse) Calendar { return resp.List[0] }, + id, changes, session, ctx, logger, acceptLanguage, + ) +} diff --git a/pkg/jmap/api_changes.go b/pkg/jmap/api_changes.go index 95aef289db..234b692141 100644 --- a/pkg/jmap/api_changes.go +++ b/pkg/jmap/api_changes.go @@ -95,9 +95,6 @@ func (j *Client) GetChanges(accountId string, session *Session, ctx context.Cont if stateMap.Addressbooks != nil { methodCalls = append(methodCalls, invocation(AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks")) } - if stateMap.Addressbooks != nil { - methodCalls = append(methodCalls, invocation(AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks")) - } if stateMap.Contacts != nil { methodCalls = append(methodCalls, invocation(ContactCardChangesCommand{AccountId: accountId, SinceState: *stateMap.Contacts, MaxChanges: posUIntPtr(maxChanges)}, "contacts")) } diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 28d2397889..bc658f3f33 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -4,59 +4,25 @@ import ( "context" "fmt" - "github.com/opencloud-eu/opencloud/pkg/jscontact" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" ) var NS_CONTACTS = ns(JmapContacts) -func (j *Client) GetContactCardsById(accountId string, session *Session, ctx context.Context, logger *log.Logger, - acceptLanguage string, contactIds []string) (map[string]jscontact.ContactCard, SessionState, State, Language, Error) { - logger = j.logger("GetContactCardsById", session, logger) - - cmd, err := j.request(session, logger, NS_CONTACTS, invocation(ContactCardGetCommand{ - Ids: contactIds, - AccountId: accountId, - }, "0")) - if err != nil { - return nil, "", "", "", err - } - - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]jscontact.ContactCard, State, Error) { - var response ContactCardGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "0", &response) - if err != nil { - return nil, "", err - } - m := map[string]jscontact.ContactCard{} - for _, contact := range response.List { - m[contact.Id] = contact - } - return m, response.State, nil - }) -} - func (j *Client) GetContactCards(accountId string, session *Session, ctx context.Context, logger *log.Logger, - acceptLanguage string, contactIds []string) ([]jscontact.ContactCard, SessionState, State, Language, Error) { + acceptLanguage string, contactIds []string) (ContactCardGetResponse, SessionState, State, Language, Error) { return get(j, "GetContactCards", NS_CONTACTS, func(accountId string, ids []string) ContactCardGetCommand { return ContactCardGetCommand{AccountId: accountId, Ids: contactIds} }, ContactCardGetResponse{}, - func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List }, + identity1, accountId, session, ctx, logger, acceptLanguage, contactIds, ) } -type ContactCardChanges struct { - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - HasMoreChanges bool `json:"hasMoreChanges"` - Created []jscontact.ContactCard `json:"created,omitempty"` - Updated []jscontact.ContactCard `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type ContactCardChanges = ChangesTemplate[ContactCard] // Retrieve the changes in Contact Cards since a given State. // @api:tags contact,changes @@ -77,8 +43,8 @@ func (j *Client) GetContactCardChanges(accountId string, session *Session, ctx c }, } }, - func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List }, - func(oldState, newState State, hasMoreChanges bool, created, updated []jscontact.ContactCard, destroyed []string) ContactCardChanges { + func(resp ContactCardGetResponse) []ContactCard { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, created, updated []ContactCard, destroyed []string) ContactCardChanges { return ContactCardChanges{ OldState: oldState, NewState: newState, @@ -94,13 +60,13 @@ func (j *Client) GetContactCardChanges(accountId string, session *Session, ctx c func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR filter ContactCardFilterElement, sortBy []ContactCardComparator, - position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) { + position uint, limit uint) (map[string][]ContactCard, SessionState, State, Language, Error) { logger = j.logger("QueryContactCards", session, logger) uniqueAccountIds := structs.Uniq(accountIds) if sortBy == nil { - sortBy = []ContactCardComparator{{Property: jscontact.ContactCardPropertyUpdated, IsAscending: false}} + sortBy = []ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}} } invocations := make([]Invocation, len(uniqueAccountIds)*2) @@ -131,8 +97,8 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co return nil, "", "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]jscontact.ContactCard, State, Error) { - resp := map[string][]jscontact.ContactCard{} + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]ContactCard, State, Error) { + resp := map[string][]ContactCard{} stateByAccountId := map[string]State{} for _, accountId := range uniqueAccountIds { var response ContactCardGetResponse @@ -150,13 +116,13 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co }) } -func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (*jscontact.ContactCard, SessionState, State, Language, Error) { +func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create ContactCard) (*ContactCard, SessionState, State, Language, Error) { logger = j.logger("CreateContactCard", session, logger) cmd, err := j.request(session, logger, NS_CONTACTS, invocation(ContactCardSetCommand{ AccountId: accountId, - Create: map[string]jscontact.ContactCard{ + Create: map[string]ContactCard{ "c": create, }, }, "0"), @@ -169,7 +135,7 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte return nil, "", "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*jscontact.ContactCard, State, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*ContactCard, State, Error) { var setResponse ContactCardSetResponse err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse) if err != nil { @@ -204,25 +170,12 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte }) } -func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { - logger = j.logger("DeleteContactCard", session, logger) - - cmd, err := j.request(session, logger, NS_CONTACTS, - invocation(ContactCardSetCommand{ - AccountId: accountId, - Destroy: destroy, - }, "0"), +func (j *Client) DeleteContactCard(accountId string, destroyIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { + return destroy(j, "DeleteContactCard", NS_CONTACTS, + func(accountId string, destroy []string) ContactCardSetCommand { + return ContactCardSetCommand{AccountId: accountId, Destroy: destroy} + }, + ContactCardSetResponse{}, + accountId, destroyIds, session, ctx, logger, acceptLanguage, ) - if err != nil { - return nil, "", "", "", err - } - - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) { - var setResponse ContactCardSetResponse - err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse) - if err != nil { - return nil, "", err - } - return setResponse.NotDestroyed, setResponse.NewState, nil - }) } diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index 8b286e6504..c7cda8fc82 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -1072,14 +1072,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx }) } -type EmailSubmissionChanges struct { - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - HasMoreChanges bool `json:"hasMoreChanges"` - Created []EmailSubmission `json:"created,omitempty"` - Updated []EmailSubmission `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type EmailSubmissionChanges = ChangesTemplate[EmailSubmission] // Retrieve the changes in Email Submissions since a given State. // @api:tags email,changes diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index 82d9bacc43..0ce515658c 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -11,23 +11,21 @@ import ( var NS_IDENTITY = ns(JmapMail) func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]Identity, SessionState, State, Language, Error) { - return get(j, "GetAllIdentities", NS_IDENTITY, + return getA(j, "GetAllIdentities", NS_IDENTITY, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId} }, IdentityGetResponse{}, - func(resp IdentityGetResponse) []Identity { return resp.List }, accountId, session, ctx, logger, acceptLanguage, []string{}, ) } func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) ([]Identity, SessionState, State, Language, Error) { - return get(j, "GetIdentities", NS_IDENTITY, + return getA(j, "GetIdentities", NS_IDENTITY, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId, Ids: ids} }, IdentityGetResponse{}, - func(resp IdentityGetResponse) []Identity { return resp.List }, accountId, session, ctx, logger, acceptLanguage, identityIds, ) } @@ -171,14 +169,7 @@ func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context. }) } -type IdentityChanges struct { - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - HasMoreChanges bool `json:"hasMoreChanges"` - Created []Identity `json:"created,omitempty"` - Updated []Identity `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type IdentityChanges = ChangesTemplate[Identity] // Retrieve the changes in Email Identities since a given State. // @api:tags email,changes diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 01cbc5ff7c..e96cbc1cea 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -11,37 +11,33 @@ import ( var NS_MAILBOX = ns(JmapMail) -type MailboxesResponse struct { - Mailboxes []Mailbox `json:"mailboxes"` - NotFound []string `json:"notFound"` -} +func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (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, + ) + */ -func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, State, Language, Error) { - return get(j, "GetMailbox", NS_MAILBOX, - func(accountId string, ids []string) MailboxGetCommand { - return MailboxGetCommand{AccountId: accountId, Ids: ids} - }, - MailboxGetResponse{}, - func(resp MailboxGetResponse) MailboxesResponse { - return MailboxesResponse{ - Mailboxes: resp.List, - NotFound: resp.NotFound, - } - }, - accountId, session, ctx, logger, acceptLanguage, ids, - ) + return fget[Mailboxes](MAILBOX, j, "GetMailbox", accountId, ids, session, ctx, logger, acceptLanguage) } func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Mailbox, SessionState, State, Language, Error) { - return getN(j, "GetAllMailboxes", NS_MAILBOX, - func(accountId string, ids []string) MailboxGetCommand { - return MailboxGetCommand{AccountId: accountId} - }, - MailboxGetResponse{}, - func(resp MailboxGetResponse) []Mailbox { return resp.List }, - identity1, - accountIds, session, ctx, logger, acceptLanguage, []string{}, - ) + /* + 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{}, session, ctx, logger, acceptLanguage) } func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter MailboxFilterElement) (map[string][]Mailbox, SessionState, State, Language, Error) { @@ -123,14 +119,7 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, }) } -type MailboxChanges struct { - HasMoreChanges bool `json:"hasMoreChanges"` - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - Created []Mailbox `json:"created,omitempty"` - Updated []Mailbox `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type MailboxChanges = ChangesTemplate[Mailbox] func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges { return MailboxChanges{ @@ -146,11 +135,12 @@ func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, u // Retrieve Mailbox changes since a given state. // @apidoc mailboxes,changes func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) { - return changes(j, "GetMailboxChanges", NS_MAILBOX, + return changesA(j, "GetMailboxChanges", NS_MAILBOX, func() MailboxChangesCommand { return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} }, MailboxChangesResponse{}, + MailboxGetResponse{}, func(path string, rof string) MailboxGetRefCommand { return MailboxGetRefCommand{ AccountId: accountId, @@ -161,7 +151,6 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte }, } }, - func(resp MailboxGetResponse) []Mailbox { return resp.List }, newMailboxChanges, session, ctx, logger, acceptLanguage, ) @@ -353,24 +342,25 @@ func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.C func (j *Client) DeleteMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, mailboxIds []string) ([]string, SessionState, State, Language, Error) { logger = j.logger("DeleteMailbox", session, logger) - cmd, err := j.request(session, logger, NS_MAILBOX, invocation(MailboxSetCommand{ + set := MailboxSetCommand{ AccountId: accountId, IfInState: ifInState, Destroy: mailboxIds, - }, "0")) + } + cmd, err := j.request(session, logger, NS_MAILBOX, invocation(set, "0")) if err != nil { return nil, "", "", "", err } return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]string, State, Error) { var setResp MailboxSetResponse - err = retrieveResponseMatchParameters(logger, body, CommandMailboxSet, "0", &setResp) + err = retrieveSet(logger, body, set, "0", &setResp) if err != nil { return nil, "", err } - setErr, notok := setResp.NotUpdated["u"] + setErr, notok := setResp.NotDestroyed["u"] if notok { - logger.Error().Msgf("%T.NotUpdated returned an error %v", setResp, setErr) + logger.Error().Msgf("%T.NotDestroyed returned an error %v", setResp, setErr) return nil, "", setErrorError(setErr, MailboxType) } return setResp.Destroyed, setResp.NewState, nil diff --git a/pkg/jmap/api_quota.go b/pkg/jmap/api_quota.go index c61a67bdc8..1f8c8deba2 100644 --- a/pkg/jmap/api_quota.go +++ b/pkg/jmap/api_quota.go @@ -20,24 +20,18 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co ) } -type QuotaChanges struct { - OldState State `json:"oldState,omitempty"` - NewState State `json:"newState"` - HasMoreChanges bool `json:"hasMoreChanges"` - Created []Quota `json:"created,omitempty"` - Updated []Quota `json:"updated,omitempty"` - Destroyed []string `json:"destroyed,omitempty"` -} +type QuotaChanges = ChangesTemplate[Quota] // Retrieve the changes in Quotas since a given State. // @api:tags quota,changes func (j *Client) GetQuotaChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (QuotaChanges, SessionState, State, Language, Error) { - return changes(j, "GetQuotaChanges", NS_QUOTA, + return changesA(j, "GetQuotaChanges", NS_QUOTA, func() QuotaChangesCommand { return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} }, QuotaChangesResponse{}, + QuotaGetResponse{}, func(path string, rof string) QuotaGetRefCommand { return QuotaGetRefCommand{ AccountId: accountId, @@ -48,7 +42,6 @@ func (j *Client) GetQuotaChanges(accountId string, session *Session, ctx context }, } }, - func(resp QuotaGetResponse) []Quota { return resp.List }, func(oldState, newState State, hasMoreChanges bool, created, updated []Quota, destroyed []string) QuotaChanges { return QuotaChanges{ OldState: oldState, diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_contact_test.go index aa8eca699c..37d60bef65 100644 --- a/pkg/jmap/integration_contact_test.go +++ b/pkg/jmap/integration_contact_test.go @@ -1,6 +1,8 @@ package jmap import ( + "context" + golog "log" "math/rand" "regexp" "slices" @@ -11,7 +13,6 @@ import ( "bytes" "encoding/base64" "fmt" - "log" "math" "strconv" "strings" @@ -19,6 +20,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/brianvoe/gofakeit/v7" "github.com/opencloud-eu/opencloud/pkg/jscontact" + "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" ) @@ -27,7 +29,7 @@ const ( EnableMediaWithBlobId = false ) -type AddressBooksBoxes struct { +type AddressBookBoxes struct { sharedReadOnly bool sharedReadWrite bool sharedDelete bool @@ -39,109 +41,34 @@ func TestAddressBooks(t *testing.T) { return } - require := require.New(t) - - s, err := newStalwartTest(t, withDirectoryQueries(true)) - require.NoError(err) - defer s.Close() - - user := pickUser() - session := s.Session(user.name) - - // we first need to retrieve the list of all the Principals in order to be able to use and test AddressBook sharing - principalIds := []string{} - { - principals, _, _, _, err := s.client.GetPrincipals(session.PrimaryAccounts.Mail, session, s.ctx, s.logger, "", []string{}) - require.NoError(err) - require.NotEmpty(principals.Principals) - principalIds = structs.Map(principals.Principals, func(p Principal) string { return p.Id }) - } - - accountId := session.PrimaryAccounts.Contacts - - ss := SessionState("") - as := EmptyState - - // we need to fetch the ID of the default AddressBook that automatically exists for each user, in order to exclude it - // from the tests below - defaultAddressBookId := "" - { - resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{}) - require.NoError(err) - require.Empty(resp.NotFound) - require.Len(resp.AddressBooks, 1) // the personal addressbook that exists by default - defaultAddressBookId = resp.AddressBooks[0].Id - ss = sessionState - as = state - } - - // we are going to create a random amount of AddressBook objects - num := uint(5 + rand.Intn(30)) - { - boxes, abooks, sessionState, state, err := s.fillAddressBook(t, accountId, num, session, user, principalIds) - require.NoError(err) - require.Len(abooks, int(num)) - ss = sessionState - as = state - - { - // lets retrieve all the existing AddressBook objects by passing an empty ID slice - resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{}) - require.NoError(err) - require.Empty(resp.NotFound) - // lets skip the default AddressBook since we did not create that one - found := structs.Filter(resp.AddressBooks, func(a AddressBook) bool { return a.Id != defaultAddressBookId }) - require.Len(found, int(num)) - m := structs.Index(found, func(a AddressBook) string { return a.Id }) - require.Len(m, int(num)) - require.Equal(sessionState, ss) - require.Equal(state, as) - - for _, a := range abooks { - require.Contains(m, a.Id) - found, ok := m[a.Id] - require.True(ok) - require.Equal(a, found) + containerTest(t, + func(session *Session) string { return session.PrimaryAccounts.Contacts }, + list, + getid, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBookGetResponse, SessionState, State, Language, Error) { + return s.client.GetAddressbooks(accountId, session, ctx, logger, acceptLanguage, ids) + }, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, change AddressBookChange) (AddressBook, SessionState, State, Language, Error) { //NOSONAR + return s.client.UpdateAddressBook(accountId, session, ctx, logger, acceptLanguage, id, change) + }, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error) { //NOSONAR + return s.client.DeleteAddressBook(accountId, ids, session, ctx, logger, acceptLanguage) + }, + func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, user User, principalIds []string) (AddressBookBoxes, []AddressBook, SessionState, State, error) { + return s.fillAddressBook(t, accountId, count, session, user, principalIds) + }, + func(orig AddressBook) AddressBookChange { + return AddressBookChange{ + Description: strPtr(orig.Description + " (changed)"), + IsSubscribed: boolPtr(!orig.IsSubscribed), } - - ss = sessionState - as = state - } - - // lets retrieve every AddressBook object we created by its ID - for _, a := range abooks { - resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{a.Id}) - require.NoError(err) - require.Empty(resp.NotFound) - require.Len(resp.AddressBooks, 1) - require.Equal(sessionState, ss) - require.Equal(state, as) - require.Equal(resp.AddressBooks[0], a) - } - - // lets modify each AddressBook - for _, a := range abooks { - changed, sessionState, state, _, err := s.client.UpdateAddressBook(accountId, session, s.ctx, s.logger, "", a.Id, AddressBookChange{Description: strPtr(a.Description + " (changed)"), IsSubscribed: boolPtr(!a.IsSubscribed)}) - require.NoError(err) - require.NotEqual(a, changed) - require.Equal(sessionState, ss) - require.NotEqual(state, as) - require.Equal(a.Description+" (changed)", changed.Description) - require.Equal(!a.IsSubscribed, changed.IsSubscribed) - } - - // now lets delete each AddressBook that we created, all at once - ids := structs.Map(abooks, func(a AddressBook) string { return a.Id }) - { - errMap, sessionState, state, _, err := s.client.DeleteAddressBook(accountId, ids, session, s.ctx, s.logger, "") - require.NoError(err) - require.Empty(errMap) - require.Equal(sessionState, ss) - require.NotEqual(state, as) - } - - allBoxesAreTicked(t, boxes) - } + }, + func(t *testing.T, orig AddressBook, _ AddressBookChange, changed AddressBook) { + require.Equal(t, orig.Name, changed.Name) + require.Equal(t, orig.Description+" (changed)", changed.Description) + require.Equal(t, !orig.IsSubscribed, changed.IsSubscribed) + }, + ) } func TestContacts(t *testing.T) { @@ -169,7 +96,7 @@ func TestContacts(t *testing.T) { InAddressBook: addressbookId, } sortBy := []ContactCardComparator{ - {Property: jscontact.ContactCardPropertyCreated, IsAscending: true}, + {Property: ContactCardPropertyCreated, IsAscending: true}, } contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, session, t.Context(), s.logger, "", filter, sortBy, 0, 0) @@ -186,6 +113,29 @@ func TestContacts(t *testing.T) { matchContact(t, actual, expected) } + // retrieve all objects at once + { + ids := structs.Map(contacts, func(c ContactCard) string { return c.Id }) + fetched, _, _, _, err := s.client.GetContactCards(accountId, session, t.Context(), s.logger, "", ids) + require.NoError(err) + require.Empty(fetched.NotFound) + require.Len(fetched.List, len(ids)) + byId := structs.Index(fetched.List, func(r ContactCard) string { return r.Id }) + for _, actual := range contacts { + expected, ok := byId[actual.Id] + require.True(ok, "failed to find created contact by its id") + matchContact(t, actual, expected) + } + } + + // retrieve each object one by one + for _, actual := range contacts { + fetched, _, _, _, err := s.client.GetContactCards(accountId, session, t.Context(), s.logger, "", []string{actual.Id}) + require.NoError(err) + require.Len(fetched.List, 1) + matchContact(t, fetched.List[0], actual) + } + exceptions := []string{} if !EnableMediaWithBlobId { exceptions = append(exceptions, "mediaWithBlobId") @@ -193,7 +143,7 @@ func TestContacts(t *testing.T) { allBoxesAreTicked(t, boxes, exceptions...) } -func matchContact(t *testing.T, actual jscontact.ContactCard, expected jscontact.ContactCard) { +func matchContact(t *testing.T, actual ContactCard, expected ContactCard) { // require.Equal(t, expected, actual) deepEqual(t, expected, actual) } @@ -222,15 +172,15 @@ func (s *StalwartTest) fillAddressBook( //NOSONAR session *Session, _ User, principalIds []string, -) (AddressBooksBoxes, []AddressBook, SessionState, State, error) { +) (AddressBookBoxes, []AddressBook, SessionState, State, error) { require := require.New(t) - boxes := AddressBooksBoxes{} + boxes := AddressBookBoxes{} created := []AddressBook{} ss := SessionState("") as := EmptyState - printer := func(s string) { log.Println(s) } + printer := func(s string) { golog.Println(s) } for i := range count { name := gofakeit.Company() @@ -295,7 +245,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR count uint, session *Session, user User, -) (string, string, map[string]jscontact.ContactCard, ContactsBoxes, error) { +) (string, string, map[string]ContactCard, ContactsBoxes, error) { require := require.New(t) c, err := NewTestJmapClient(session, user.name, user.password, true, true) require.NoError(err) @@ -303,7 +253,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR boxes := ContactsBoxes{} - printer := func(s string) { log.Println(s) } + printer := func(s string) { golog.Println(s) } accountId := c.session.PrimaryAccounts.Contacts require.NotEmpty(accountId, "no primary account for contacts in session") @@ -331,7 +281,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR } require.NotEmpty(addressbookId) - filled := map[string]jscontact.ContactCard{} + filled := map[string]ContactCard{} for i := range count { person := gofakeit.Person() nameMap, nameObj := createName(person) @@ -345,7 +295,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR "kind": "individual", "name": nameMap, } - card := jscontact.ContactCard{ + card := ContactCard{ Type: jscontact.ContactCardType, Version: "1.0", AddressBookIds: toBoolMap([]string{addressbookId}), diff --git a/pkg/jmap/integration_event_test.go b/pkg/jmap/integration_event_test.go index fa555b54b6..dc40cac92f 100644 --- a/pkg/jmap/integration_event_test.go +++ b/pkg/jmap/integration_event_test.go @@ -1,10 +1,11 @@ package jmap import ( + "context" "encoding/base64" "encoding/json" "fmt" - "log" + golog "log" "math" "math/rand" "strconv" @@ -16,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "github.com/opencloud-eu/opencloud/pkg/jscalendar" + "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" ) @@ -25,6 +27,41 @@ const ( EnableEventParticipantDescriptionFields = false ) +func TestCalendars(t *testing.T) { //NOSONAR + if skip(t) { + return + } + + containerTest(t, + func(session *Session) string { return session.PrimaryAccounts.Calendars }, + func(resp CalendarGetResponse) []Calendar { return resp.List }, + func(obj Calendar) string { return obj.Id }, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarGetResponse, SessionState, State, Language, Error) { + return s.client.GetCalendars(accountId, session, ctx, logger, acceptLanguage, ids) + }, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, change CalendarChange) (Calendar, SessionState, State, Language, Error) { //NOSONAR + return s.client.UpdateCalendar(accountId, session, ctx, logger, acceptLanguage, id, change) + }, + func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error) { //NOSONAR + return s.client.DeleteCalendar(accountId, ids, session, ctx, logger, acceptLanguage) + }, + func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, user User, principalIds []string) (CalendarBoxes, []Calendar, SessionState, State, error) { + return s.fillCalendar(t, accountId, count, session, user, principalIds) + }, + func(orig Calendar) CalendarChange { + return CalendarChange{ + Description: strPtr(orig.Description + " (changed)"), + IsSubscribed: boolPtr(!orig.IsSubscribed), + } + }, + func(t *testing.T, orig Calendar, _ CalendarChange, changed Calendar) { + require.Equal(t, orig.Name, changed.Name) + require.Equal(t, orig.Description+" (changed)", changed.Description) + require.Equal(t, !orig.IsSubscribed, changed.IsSubscribed) + }, + ) +} + func TestEvents(t *testing.T) { if skip(t) { return @@ -79,6 +116,145 @@ func matchEvent(t *testing.T, actual CalendarEvent, expected CalendarEvent) { deepEqual(t, expected, actual) } +type CalendarBoxes struct { + sharedReadOnly bool + sharedReadWrite bool + sharedDelete bool + sortOrdered bool +} + +func (s *StalwartTest) fillCalendar( //NOSONAR + t *testing.T, + accountId string, + count uint, + session *Session, + _ User, + principalIds []string, +) (CalendarBoxes, []Calendar, SessionState, State, error) { + require := require.New(t) + + boxes := CalendarBoxes{} + created := []Calendar{} + ss := SessionState("") + as := EmptyState + + printer := func(s string) { golog.Println(s) } + + for i := range count { + name := gofakeit.Company() + description := gofakeit.SentenceSimple() + subscribed := gofakeit.Bool() + visible := gofakeit.Bool() + color := gofakeit.HexColor() + include := pickRandom(IncludeInAvailabilities...) + dawtId := gofakeit.UUID() + daotId := gofakeit.UUID() + cal := CalendarChange{ + Name: &name, + Description: &description, + IsSubscribed: &subscribed, + Color: &color, + IsVisible: &visible, + IncludeInAvailability: &include, + DefaultAlertsWithTime: map[string]jscalendar.Alert{ + dawtId: { + Type: jscalendar.AlertType, + Trigger: jscalendar.OffsetTrigger{ + Type: jscalendar.OffsetTriggerType, + Offset: "-PT5M", + RelativeTo: jscalendar.RelativeToStart, + }, + Action: jscalendar.AlertActionDisplay, + }, + }, + DefaultAlertsWithoutTime: map[string]jscalendar.Alert{ + daotId: { + Type: jscalendar.AlertType, + Trigger: jscalendar.OffsetTrigger{ + Type: jscalendar.OffsetTriggerType, + Offset: "-PT24H", + RelativeTo: jscalendar.RelativeToStart, + }, + Action: jscalendar.AlertActionDisplay, + }, + }, + } + if i%2 == 0 { + cal.SortOrder = posUIntPtr(gofakeit.Uint()) + boxes.sortOrdered = true + } + var sharing *CalendarRights = nil + switch i % 4 { + default: + // no sharing + case 1: + sharing = &CalendarRights{ + MayReadFreeBusy: true, + MayReadItems: true, + MayRSVP: true, + MayAdmin: false, + MayDelete: false, + MayWriteAll: false, + MayWriteOwn: false, + MayUpdatePrivate: false, + } + boxes.sharedReadWrite = true + case 2: + sharing = &CalendarRights{ + MayReadFreeBusy: true, + MayReadItems: true, + MayRSVP: true, + MayAdmin: false, + MayDelete: false, + MayWriteAll: false, + MayWriteOwn: true, + MayUpdatePrivate: true, + } + boxes.sharedReadOnly = true + case 3: + sharing = &CalendarRights{ + MayReadFreeBusy: true, + MayReadItems: true, + MayRSVP: true, + MayAdmin: false, + MayDelete: true, + MayWriteAll: true, + MayWriteOwn: true, + MayUpdatePrivate: true, + } + boxes.sharedDelete = true + } + if sharing != nil { + numPrincipals := 1 + rand.Intn(len(principalIds)-1) + m := make(map[string]CalendarRights, numPrincipals) + for _, p := range pickRandomN(numPrincipals, principalIds...) { + m[p] = *sharing + } + cal.ShareWith = m + } + + a, sessionState, state, _, err := s.client.CreateCalendar(accountId, session, s.ctx, s.logger, "", cal) + if err != nil { + return boxes, created, ss, as, err + } + require.NotEmpty(sessionState) + require.NotEmpty(state) + if ss != SessionState("") { + require.Equal(ss, sessionState) + } + if as != EmptyState { + require.NotEqual(as, state) + } + require.NotNil(a) + created = append(created, *a) + ss = sessionState + as = state + + printer(fmt.Sprintf("📅 created %*s/%v id=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, a.Id)) + } + return boxes, created, ss, as, nil +} + type EventsBoxes struct { categories bool keywords bool @@ -98,7 +274,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR boxes := EventsBoxes{} - printer := func(s string) { log.Println(s) } + printer := func(s string) { golog.Println(s) } accountId := c.session.PrimaryAccounts.Calendars require.NotEmpty(accountId, "no primary account for calendars in session") diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go index 1953d2f2d6..64ffc030a6 100644 --- a/pkg/jmap/integration_test.go +++ b/pkg/jmap/integration_test.go @@ -1141,7 +1141,7 @@ func pickUser() User { } func pickRandoms[T any](s ...T) []T { - return pickRandomN[T](rand.Intn(len(s)), s...) + return pickRandomN(rand.Intn(len(s)), s...) } func pickRandomN[T any](n int, s ...T) []T { @@ -1213,3 +1213,125 @@ func deepEqual[T any](t *testing.T, expected, actual T) { } require.Empty(t, diff) } + +func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](t *testing.T, //NOSONAR + acc func(session *Session) string, + obj func(RESP) []OBJ, + id func(OBJ) string, + get func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error), + update func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, id string, change CHANGE) (OBJ, SessionState, State, Language, Error), + destroy func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error), + fill func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, _ User, principalIds []string) (BOXES, []OBJ, SessionState, State, error), + change func(OBJ) CHANGE, + checkChanged func(t *testing.T, orig OBJ, change CHANGE, changed OBJ), +) { + require := require.New(t) + + s, err := newStalwartTest(t, withDirectoryQueries(true)) + require.NoError(err) + defer s.Close() + + user := pickUser() + session := s.Session(user.name) + + accountId := acc(session) + + // we first need to retrieve the list of all the Principals in order to be able to use and test sharing + principalIds := []string{} + { + principals, _, _, _, err := s.client.GetPrincipals(accountId, session, s.ctx, s.logger, "", []string{}) + require.NoError(err) + require.NotEmpty(principals.Principals) + principalIds = structs.Map(principals.Principals, func(p Principal) string { return p.Id }) + } + + ss := SessionState("") + as := EmptyState + + // we need to fetch the ID of the default object that automatically exists for each user, in order to exclude it + // from the tests below + defaultContainerId := "" + { + resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{}) + require.NoError(err) + require.Empty(resp.GetNotFound()) + objs := obj(resp) + require.Len(objs, 1) // the personal calendar that exists by default + defaultContainerId = id(objs[0]) + ss = sessionState + as = state + } + + // we are going to create a random amount of objects + num := uint(5 + rand.Intn(30)) + { + boxes, all, sessionState, state, err := fill(s, t, accountId, num, session, user, principalIds) + require.NoError(err) + require.Len(all, int(num)) + ss = sessionState + as = state + + { + // lets retrieve all the existing objects by passing an empty ID slice + resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{}) + require.NoError(err) + require.Empty(resp.GetNotFound()) + objs := obj(resp) + // lets skip the default object since we did not create that one + found := structs.Filter(objs, func(a OBJ) bool { return id(a) != defaultContainerId }) + require.Len(found, int(num)) + m := structs.Index(found, id) + require.Len(m, int(num)) + require.Equal(sessionState, ss) + require.Equal(state, as) + + for _, a := range all { + i := id(a) + require.Contains(m, i) + found, ok := m[i] + require.True(ok) + require.Equal(a, found) + } + + ss = sessionState + as = state + } + + // lets retrieve every object we created by its ID + for _, a := range all { + i := id(a) + resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{i}) + require.NoError(err) + require.Empty(resp.GetNotFound()) + objs := obj(resp) + require.Len(objs, 1) + require.Equal(sessionState, ss) + require.Equal(state, as) + require.Equal(objs[0], a) + } + + // lets modify each AddressBook + for _, a := range all { + i := id(a) + ch := change(a) + changed, sessionState, state, _, err := update(s, accountId, session, s.ctx, s.logger, "", i, ch) + require.NoError(err) + require.NotEqual(a, changed) + require.Equal(sessionState, ss) + require.NotEqual(state, as) + checkChanged(t, a, ch, changed) + } + + // now lets delete each object that we created, all at once + ids := structs.Map(all, id) + { + errMap, sessionState, state, _, err := destroy(s, accountId, session, s.ctx, s.logger, "", ids) + require.NoError(err) + require.Empty(errMap) + require.Equal(sessionState, ss) + require.NotEqual(state, as) + } + + allBoxesAreTicked(t, boxes) + } +} diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index c56757da45..a09e59ffce 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -1230,6 +1230,15 @@ type Response struct { RequestId string `json:"requestId,omitempty"` } +type Foo interface { + GetObjectType() ObjectType +} + +type Idable interface { + GetId() string + Foo +} + // Patch Object. // // Example: @@ -1301,45 +1310,48 @@ type JmapCommand interface { GetObjectType() ObjectType } -type GetCommand interface { +type GetCommand[T Foo] interface { JmapCommand - GetResponse() GetResponse + GetResponse() GetResponse[T] } -type GetResponse interface { +type GetResponse[T Foo] interface { GetState() State GetNotFound() []string + GetList() []T } -type SetCommand interface { +type SetCommand[T Foo] interface { JmapCommand - GetResponse() SetResponse + GetResponse() SetResponse[T] } -type SetResponse interface { +type SetResponse[T Foo] interface { GetNotCreated() map[string]SetError GetNotUpdated() map[string]SetError GetNotDestroyed() map[string]SetError GetOldState() State GetNewState() State + GetMarker() T } type Change interface { AsPatch() PatchObject } -type ChangesCommand interface { +type ChangesCommand[T Foo] interface { JmapCommand - GetResponse() ChangesResponse + GetResponse() ChangesResponse[T] } -type ChangesResponse interface { +type ChangesResponse[T Foo] interface { GetOldState() State GetNewState() State GetHasMoreChanges() bool GetCreated() []string GetUpdated() []string GetDestroyed() []string + GetMarker() T } type QueryCommand interface { @@ -1351,6 +1363,15 @@ type QueryResponse interface { GetQueryState() State } +type ChangesTemplate[T Foo] struct { + HasMoreChanges bool `json:"hasMoreChanges"` + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + Created []T `json:"created,omitempty"` + Updated []T `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` +} + type FilterOperatorTerm string const ( @@ -1503,6 +1524,11 @@ type Mailbox struct { IsSubscribed *bool `json:"isSubscribed,omitempty"` } +var _ Idable = &Mailbox{} + +func (f Mailbox) GetObjectType() ObjectType { return MailboxType } +func (f Mailbox) GetId() string { return f.Id } + type MailboxChange struct { // User-visible name for the Mailbox, e.g., “Inbox”. // @@ -1595,22 +1621,22 @@ type MailboxGetCommand struct { Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &MailboxGetCommand{} +var _ GetCommand[Mailbox] = &MailboxGetCommand{} -func (c MailboxGetCommand) GetCommand() Command { return CommandMailboxGet } -func (c MailboxGetCommand) GetObjectType() ObjectType { return MailboxType } -func (c MailboxGetCommand) GetResponse() GetResponse { return MailboxGetResponse{} } +func (c MailboxGetCommand) GetCommand() Command { return CommandMailboxGet } +func (c MailboxGetCommand) GetObjectType() ObjectType { return MailboxType } +func (c MailboxGetCommand) GetResponse() GetResponse[Mailbox] { return MailboxGetResponse{} } type MailboxGetRefCommand struct { AccountId string `json:"accountId"` IdsRef *ResultReference `json:"#ids,omitempty"` } -var _ GetCommand = &MailboxGetRefCommand{} +var _ GetCommand[Mailbox] = &MailboxGetRefCommand{} -func (c MailboxGetRefCommand) GetCommand() Command { return CommandMailboxGet } -func (c MailboxGetRefCommand) GetObjectType() ObjectType { return MailboxType } -func (c MailboxGetRefCommand) GetResponse() GetResponse { return MailboxGetResponse{} } +func (c MailboxGetRefCommand) GetCommand() Command { return CommandMailboxGet } +func (c MailboxGetRefCommand) GetObjectType() ObjectType { return MailboxType } +func (c MailboxGetRefCommand) GetResponse() GetResponse[Mailbox] { return MailboxGetResponse{} } type MailboxSetCommand struct { AccountId string `json:"accountId"` @@ -1620,11 +1646,11 @@ type MailboxSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &MailboxSetCommand{} +var _ SetCommand[Mailbox] = &MailboxSetCommand{} -func (c MailboxSetCommand) GetCommand() Command { return CommandMailboxSet } -func (c MailboxSetCommand) GetObjectType() ObjectType { return MailboxType } -func (c MailboxSetCommand) GetResponse() SetResponse { return MailboxSetResponse{} } +func (c MailboxSetCommand) GetCommand() Command { return CommandMailboxSet } +func (c MailboxSetCommand) GetObjectType() ObjectType { return MailboxType } +func (c MailboxSetCommand) GetResponse() SetResponse[Mailbox] { return MailboxSetResponse{} } type MailboxSetResponse struct { AccountId string `json:"accountId"` @@ -1638,13 +1664,14 @@ type MailboxSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &MailboxSetResponse{} +var _ SetResponse[Mailbox] = &MailboxSetResponse{} func (r MailboxSetResponse) GetOldState() State { return r.OldState } func (r MailboxSetResponse) GetNewState() State { return r.NewState } func (r MailboxSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r MailboxSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r MailboxSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r MailboxSetResponse) GetMarker() Mailbox { return Mailbox{} } type MailboxFilterElement interface { _isAMailboxFilterElement() // marker method @@ -2052,11 +2079,11 @@ type EmailGetCommand struct { MaxBodyValueBytes uint `json:"maxBodyValueBytes,omitempty"` } -var _ GetCommand = &EmailGetCommand{} +var _ GetCommand[Email] = &EmailGetCommand{} -func (c EmailGetCommand) GetCommand() Command { return CommandEmailGet } -func (c EmailGetCommand) GetObjectType() ObjectType { return EmailType } -func (c EmailGetCommand) GetResponse() GetResponse { return EmailGetResponse{} } +func (c EmailGetCommand) GetCommand() Command { return CommandEmailGet } +func (c EmailGetCommand) GetObjectType() ObjectType { return EmailType } +func (c EmailGetCommand) GetResponse() GetResponse[Email] { return EmailGetResponse{} } type EmailGetRefCommand struct { // The ids of the Email objects to return. @@ -2114,11 +2141,11 @@ type EmailGetRefCommand struct { MaxBodyValueBytes uint `json:"maxBodyValueBytes,omitempty"` } -var _ GetCommand = &EmailGetRefCommand{} +var _ GetCommand[Email] = &EmailGetRefCommand{} -func (c EmailGetRefCommand) GetCommand() Command { return CommandEmailGet } -func (c EmailGetRefCommand) GetObjectType() ObjectType { return EmailType } -func (c EmailGetRefCommand) GetResponse() GetResponse { return EmailGetResponse{} } +func (c EmailGetRefCommand) GetCommand() Command { return CommandEmailGet } +func (c EmailGetRefCommand) GetObjectType() ObjectType { return EmailType } +func (c EmailGetRefCommand) GetResponse() GetResponse[Email] { return EmailGetResponse{} } type EmailChangesCommand struct { // The id of the account to use. @@ -2138,11 +2165,11 @@ type EmailChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitempty"` } -var _ ChangesCommand = &EmailChangesCommand{} +var _ ChangesCommand[Email] = &EmailChangesCommand{} -func (c EmailChangesCommand) GetCommand() Command { return CommandEmailChanges } -func (c EmailChangesCommand) GetObjectType() ObjectType { return EmailType } -func (c EmailChangesCommand) GetResponse() ChangesResponse { return EmailChangesResponse{} } +func (c EmailChangesCommand) GetCommand() Command { return CommandEmailChanges } +func (c EmailChangesCommand) GetObjectType() ObjectType { return EmailType } +func (c EmailChangesCommand) GetResponse() ChangesResponse[Email] { return EmailChangesResponse{} } type EmailAddress struct { // The display-name of the mailbox [RFC5322](https://www.rfc-editor.org/rfc/rfc5322.html). @@ -2555,6 +2582,11 @@ type Email struct { Preview string `json:"preview,omitempty"` } +var _ Idable = &Email{} + +func (f Email) GetObjectType() ObjectType { return EmailType } +func (f Email) GetId() string { return f.Id } + type AddressParameters struct { HoldUntil time.Time `json:"HOLDUNTIL,omitzero"` HoldForSeconds uint `json:"HOLDFOR,omitzero"` @@ -2736,6 +2768,11 @@ type EmailSubmission struct { MdnBlobIds []string `json:"mdnBlobIds,omitempty"` } +var _ Idable = &EmailSubmission{} + +func (f EmailSubmission) GetObjectType() ObjectType { return EmailSubmissionType } +func (f EmailSubmission) GetId() string { return f.Id } + type EmailSubmissionGetCommand struct { // The id of the account to use. AccountId string `json:"accountId"` @@ -2754,11 +2791,13 @@ type EmailSubmissionGetCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &EmailSubmissionGetCommand{} +var _ GetCommand[EmailSubmission] = &EmailSubmissionGetCommand{} func (c EmailSubmissionGetCommand) GetCommand() Command { return CommandEmailSubmissionGet } func (c EmailSubmissionGetCommand) GetObjectType() ObjectType { return EmailSubmissionType } -func (c EmailSubmissionGetCommand) GetResponse() GetResponse { return EmailSubmissionGetResponse{} } +func (c EmailSubmissionGetCommand) GetResponse() GetResponse[EmailSubmission] { + return EmailSubmissionGetResponse{} +} type EmailSubmissionGetRefCommand struct { // The id of the account to use. @@ -2778,11 +2817,13 @@ type EmailSubmissionGetRefCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &EmailSubmissionGetRefCommand{} +var _ GetCommand[EmailSubmission] = &EmailSubmissionGetRefCommand{} func (c EmailSubmissionGetRefCommand) GetCommand() Command { return CommandEmailSubmissionGet } func (c EmailSubmissionGetRefCommand) GetObjectType() ObjectType { return EmailSubmissionType } -func (c EmailSubmissionGetRefCommand) GetResponse() GetResponse { return EmailSubmissionGetResponse{} } +func (c EmailSubmissionGetRefCommand) GetResponse() GetResponse[EmailSubmission] { + return EmailSubmissionGetResponse{} +} type EmailSubmissionGetResponse struct { // The id of the account used for the call. @@ -2816,10 +2857,11 @@ type EmailSubmissionGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &EmailSubmissionGetResponse{} +var _ GetResponse[EmailSubmission] = &EmailSubmissionGetResponse{} -func (r EmailSubmissionGetResponse) GetState() State { return r.State } -func (r EmailSubmissionGetResponse) GetNotFound() []string { return r.NotFound } +func (r EmailSubmissionGetResponse) GetState() State { return r.State } +func (r EmailSubmissionGetResponse) GetNotFound() []string { return r.NotFound } +func (r EmailSubmissionGetResponse) GetList() []EmailSubmission { return r.List } type EmailSubmissionChangesCommand struct { // The id of the account to use. @@ -2844,11 +2886,11 @@ type EmailSubmissionChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &EmailSubmissionChangesCommand{} +var _ ChangesCommand[EmailSubmission] = &EmailSubmissionChangesCommand{} func (c EmailSubmissionChangesCommand) GetCommand() Command { return CommandEmailSubmissionChanges } func (c EmailSubmissionChangesCommand) GetObjectType() ObjectType { return EmailSubmissionType } -func (c EmailSubmissionChangesCommand) GetResponse() ChangesResponse { +func (c EmailSubmissionChangesCommand) GetResponse() ChangesResponse[EmailSubmission] { return EmailSubmissionChangesResponse{} } @@ -2877,14 +2919,15 @@ type EmailSubmissionChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &EmailSubmissionChangesResponse{} +var _ ChangesResponse[EmailSubmission] = &EmailSubmissionChangesResponse{} -func (r EmailSubmissionChangesResponse) GetOldState() State { return r.OldState } -func (r EmailSubmissionChangesResponse) GetNewState() State { return r.NewState } -func (r EmailSubmissionChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges } -func (r EmailSubmissionChangesResponse) GetCreated() []string { return r.Created } -func (r EmailSubmissionChangesResponse) GetUpdated() []string { return r.Updated } -func (r EmailSubmissionChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r EmailSubmissionChangesResponse) GetOldState() State { return r.OldState } +func (r EmailSubmissionChangesResponse) GetNewState() State { return r.NewState } +func (r EmailSubmissionChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges } +func (r EmailSubmissionChangesResponse) GetCreated() []string { return r.Created } +func (r EmailSubmissionChangesResponse) GetUpdated() []string { return r.Updated } +func (r EmailSubmissionChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r EmailSubmissionChangesResponse) GetMarker() EmailSubmission { return EmailSubmission{} } // same as EmailSubmission but without the server-set attributes type EmailSubmissionCreate struct { @@ -2922,11 +2965,13 @@ type EmailSubmissionSetCommand struct { OnSuccessDestroyEmail []string `json:"onSuccessDestroyEmail,omitempty"` } -var _ SetCommand = &EmailSubmissionSetCommand{} +var _ SetCommand[EmailSubmission] = &EmailSubmissionSetCommand{} func (c EmailSubmissionSetCommand) GetCommand() Command { return CommandEmailSubmissionSet } func (c EmailSubmissionSetCommand) GetObjectType() ObjectType { return EmailSubmissionType } -func (c EmailSubmissionSetCommand) GetResponse() SetResponse { return EmailSubmissionSetResponse{} } +func (c EmailSubmissionSetCommand) GetResponse() SetResponse[EmailSubmission] { + return EmailSubmissionSetResponse{} +} type CreatedEmailSubmission struct { Id string `json:"id"` @@ -2959,13 +3004,14 @@ type EmailSubmissionSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &EmailSubmissionSetResponse{} +var _ SetResponse[EmailSubmission] = &EmailSubmissionSetResponse{} func (r EmailSubmissionSetResponse) GetOldState() State { return r.OldState } func (r EmailSubmissionSetResponse) GetNewState() State { return r.NewState } func (r EmailSubmissionSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r EmailSubmissionSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r EmailSubmissionSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r EmailSubmissionSetResponse) GetMarker() EmailSubmission { return EmailSubmission{} } type EmailQueryResponse struct { // The id of the account used for the call. @@ -3048,10 +3094,12 @@ type EmailGetResponse struct { NotFound []string `json:"notFound"` } -var _ GetResponse = &EmailGetResponse{} +var _ GetResponse[Email] = &EmailGetResponse{} func (r EmailGetResponse) GetState() State { return r.State } func (r EmailGetResponse) GetNotFound() []string { return r.NotFound } +func (r EmailGetResponse) GetList() []Email { return r.List } +func (r EmailGetResponse) GetMarker() Email { return Email{} } type EmailChangesResponse struct { // The id of the account used for the call. @@ -3077,7 +3125,7 @@ type EmailChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &EmailChangesResponse{} +var _ ChangesResponse[Email] = &EmailChangesResponse{} func (r EmailChangesResponse) GetOldState() State { return r.OldState } func (r EmailChangesResponse) GetNewState() State { return r.NewState } @@ -3085,6 +3133,7 @@ func (r EmailChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges func (r EmailChangesResponse) GetCreated() []string { return r.Created } func (r EmailChangesResponse) GetUpdated() []string { return r.Updated } func (r EmailChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r EmailChangesResponse) GetMarker() Email { return Email{} } type MailboxGetResponse struct { // The id of the account used for the call. @@ -3110,10 +3159,11 @@ type MailboxGetResponse struct { NotFound []string `json:"notFound"` } -var _ GetResponse = &MailboxGetResponse{} +var _ GetResponse[Mailbox] = &MailboxGetResponse{} func (r MailboxGetResponse) GetState() State { return r.State } func (r MailboxGetResponse) GetNotFound() []string { return r.NotFound } +func (r MailboxGetResponse) GetList() []Mailbox { return r.List } type MailboxChangesCommand struct { // The id of the account to use. @@ -3138,11 +3188,13 @@ type MailboxChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &MailboxChangesCommand{} +var _ ChangesCommand[Mailbox] = &MailboxChangesCommand{} -func (c MailboxChangesCommand) GetCommand() Command { return CommandMailboxChanges } -func (c MailboxChangesCommand) GetObjectType() ObjectType { return MailboxType } -func (c MailboxChangesCommand) GetResponse() ChangesResponse { return MailboxChangesResponse{} } +func (c MailboxChangesCommand) GetCommand() Command { return CommandMailboxChanges } +func (c MailboxChangesCommand) GetObjectType() ObjectType { return MailboxType } +func (c MailboxChangesCommand) GetResponse() ChangesResponse[Mailbox] { + return MailboxChangesResponse{} +} type MailboxChangesResponse struct { // The id of the account used for the call. @@ -3175,7 +3227,7 @@ type MailboxChangesResponse struct { UpdatedProperties []string `json:"updatedProperties,omitempty"` } -var _ ChangesResponse = &MailboxChangesResponse{} +var _ ChangesResponse[Mailbox] = &MailboxChangesResponse{} func (r MailboxChangesResponse) GetOldState() State { return r.OldState } func (r MailboxChangesResponse) GetNewState() State { return r.NewState } @@ -3183,6 +3235,7 @@ func (r MailboxChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChang func (r MailboxChangesResponse) GetCreated() []string { return r.Created } func (r MailboxChangesResponse) GetUpdated() []string { return r.Updated } func (r MailboxChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r MailboxChangesResponse) GetMarker() Mailbox { return Mailbox{} } type MailboxQueryResponse struct { // The id of the account used for the call. @@ -3392,11 +3445,11 @@ type EmailSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &EmailSetCommand{} +var _ SetCommand[Email] = &EmailSetCommand{} -func (c EmailSetCommand) GetCommand() Command { return CommandEmailSet } -func (c EmailSetCommand) GetObjectType() ObjectType { return EmailType } -func (c EmailSetCommand) GetResponse() SetResponse { return EmailSubmissionSetResponse{} } +func (c EmailSetCommand) GetCommand() Command { return CommandEmailSet } +func (c EmailSetCommand) GetObjectType() ObjectType { return EmailType } +func (c EmailSetCommand) GetResponse() SetResponse[Email] { return EmailSetResponse{} } type EmailSetResponse struct { // The id of the account used for the call. @@ -3445,13 +3498,14 @@ type EmailSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &EmailSetResponse{} +var _ SetResponse[Email] = &EmailSetResponse{} func (r EmailSetResponse) GetOldState() State { return r.OldState } func (r EmailSetResponse) GetNewState() State { return r.NewState } func (r EmailSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r EmailSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r EmailSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r EmailSetResponse) GetMarker() Email { return Email{} } const ( EmailMimeType = "message/rfc822" @@ -3543,27 +3597,32 @@ type Thread struct { EmailIds []string } +var _ Idable = &Thread{} + +func (f Thread) GetObjectType() ObjectType { return ThreadType } +func (f Thread) GetId() string { return f.Id } + type ThreadGetCommand struct { AccountId string `json:"accountId"` Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &ThreadGetCommand{} +var _ GetCommand[Thread] = &ThreadGetCommand{} -func (c ThreadGetCommand) GetCommand() Command { return CommandThreadGet } -func (c ThreadGetCommand) GetObjectType() ObjectType { return ThreadType } -func (c ThreadGetCommand) GetResponse() GetResponse { return ThreadGetResponse{} } +func (c ThreadGetCommand) GetCommand() Command { return CommandThreadGet } +func (c ThreadGetCommand) GetObjectType() ObjectType { return ThreadType } +func (c ThreadGetCommand) GetResponse() GetResponse[Thread] { return ThreadGetResponse{} } type ThreadGetRefCommand struct { AccountId string `json:"accountId"` IdsRef *ResultReference `json:"#ids,omitempty"` } -var _ GetCommand = &ThreadGetRefCommand{} +var _ GetCommand[Thread] = &ThreadGetRefCommand{} -func (c ThreadGetRefCommand) GetCommand() Command { return CommandThreadGet } -func (c ThreadGetRefCommand) GetObjectType() ObjectType { return ThreadType } -func (c ThreadGetRefCommand) GetResponse() GetResponse { return ThreadGetResponse{} } +func (c ThreadGetRefCommand) GetCommand() Command { return CommandThreadGet } +func (c ThreadGetRefCommand) GetObjectType() ObjectType { return ThreadType } +func (c ThreadGetRefCommand) GetResponse() GetResponse[Thread] { return ThreadGetResponse{} } type ThreadGetResponse struct { AccountId string @@ -3572,21 +3631,22 @@ type ThreadGetResponse struct { NotFound []string } -var _ GetResponse = &ThreadGetResponse{} +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 } type IdentityGetCommand struct { AccountId string `json:"accountId"` Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &IdentityGetCommand{} +var _ GetCommand[Identity] = &IdentityGetCommand{} -func (c IdentityGetCommand) GetCommand() Command { return CommandIdentityGet } -func (c IdentityGetCommand) GetObjectType() ObjectType { return IdentityType } -func (c IdentityGetCommand) GetResponse() GetResponse { return IdentityGetResponse{} } +func (c IdentityGetCommand) GetCommand() Command { return CommandIdentityGet } +func (c IdentityGetCommand) GetObjectType() ObjectType { return IdentityType } +func (c IdentityGetCommand) GetResponse() GetResponse[Identity] { return IdentityGetResponse{} } type IdentityGetRefCommand struct { AccountId string `json:"accountId"` @@ -3594,11 +3654,11 @@ type IdentityGetRefCommand struct { PropertiesRef *ResultReference `json:"#properties,omitempty"` } -var _ GetCommand = &IdentityGetRefCommand{} +var _ GetCommand[Identity] = &IdentityGetRefCommand{} -func (c IdentityGetRefCommand) GetCommand() Command { return CommandIdentityGet } -func (c IdentityGetRefCommand) GetObjectType() ObjectType { return IdentityType } -func (c IdentityGetRefCommand) GetResponse() GetResponse { return IdentityGetResponse{} } +func (c IdentityGetRefCommand) GetCommand() Command { return CommandIdentityGet } +func (c IdentityGetRefCommand) GetObjectType() ObjectType { return IdentityType } +func (c IdentityGetRefCommand) GetResponse() GetResponse[Identity] { return IdentityGetResponse{} } type IdentityChangesCommand struct { // The id of the account to use. @@ -3623,11 +3683,13 @@ type IdentityChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &IdentityChangesCommand{} +var _ ChangesCommand[Identity] = &IdentityChangesCommand{} -func (c IdentityChangesCommand) GetCommand() Command { return CommandIdentityChanges } -func (c IdentityChangesCommand) GetObjectType() ObjectType { return IdentityType } -func (c IdentityChangesCommand) GetResponse() ChangesResponse { return IdentityChangesResponse{} } +func (c IdentityChangesCommand) GetCommand() Command { return CommandIdentityChanges } +func (c IdentityChangesCommand) GetObjectType() ObjectType { return IdentityType } +func (c IdentityChangesCommand) GetResponse() ChangesResponse[Identity] { + return IdentityChangesResponse{} +} type IdentityChangesResponse struct { // The id of the account used for the call. @@ -3654,7 +3716,7 @@ type IdentityChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &IdentityChangesResponse{} +var _ ChangesResponse[Identity] = &IdentityChangesResponse{} func (r IdentityChangesResponse) GetOldState() State { return r.OldState } func (r IdentityChangesResponse) GetNewState() State { return r.NewState } @@ -3662,6 +3724,7 @@ func (r IdentityChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChan func (r IdentityChangesResponse) GetCreated() []string { return r.Created } func (r IdentityChangesResponse) GetUpdated() []string { return r.Updated } func (r IdentityChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r IdentityChangesResponse) GetMarker() Identity { return Identity{} } type IdentitySetCommand struct { AccountId string `json:"accountId"` @@ -3671,11 +3734,11 @@ type IdentitySetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &IdentitySetCommand{} +var _ SetCommand[Identity] = &IdentitySetCommand{} -func (c IdentitySetCommand) GetCommand() Command { return CommandIdentitySet } -func (c IdentitySetCommand) GetObjectType() ObjectType { return IdentityType } -func (c IdentitySetCommand) GetResponse() SetResponse { return IdentitySetResponse{} } +func (c IdentitySetCommand) GetCommand() Command { return CommandIdentitySet } +func (c IdentitySetCommand) GetObjectType() ObjectType { return IdentityType } +func (c IdentitySetCommand) GetResponse() SetResponse[Identity] { return IdentitySetResponse{} } type IdentitySetResponse struct { AccountId string `json:"accountId"` @@ -3689,13 +3752,14 @@ type IdentitySetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &IdentitySetResponse{} +var _ SetResponse[Identity] = &IdentitySetResponse{} func (r IdentitySetResponse) GetOldState() State { return r.OldState } func (r IdentitySetResponse) GetNewState() State { return r.NewState } func (r IdentitySetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r IdentitySetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r IdentitySetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r IdentitySetResponse) GetMarker() Identity { return Identity{} } // An Identity object stores information about an email address or domain the user may send from. type Identity struct { @@ -3741,6 +3805,11 @@ type Identity struct { MayDelete bool `json:"mayDelete,omitzero"` } +var _ Idable = &Identity{} + +func (f Identity) GetObjectType() ObjectType { return IdentityType } +func (f Identity) GetId() string { return f.Id } + var _ Change = Identity{} func (i Identity) AsPatch() PatchObject { @@ -3773,20 +3842,23 @@ type IdentityGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &IdentityGetResponse{} +var _ GetResponse[Identity] = &IdentityGetResponse{} func (r IdentityGetResponse) GetState() State { return r.State } func (r IdentityGetResponse) GetNotFound() []string { return r.NotFound } +func (r IdentityGetResponse) GetList() []Identity { return r.List } type VacationResponseGetCommand struct { AccountId string `json:"accountId"` } -var _ GetCommand = &VacationResponseGetCommand{} +var _ GetCommand[VacationResponse] = &VacationResponseGetCommand{} func (c VacationResponseGetCommand) GetCommand() Command { return CommandVacationResponseGet } func (c VacationResponseGetCommand) GetObjectType() ObjectType { return VacationResponseType } -func (c VacationResponseGetCommand) GetResponse() GetResponse { return VacationResponseGetResponse{} } +func (c VacationResponseGetCommand) GetResponse() GetResponse[VacationResponse] { + return VacationResponseGetResponse{} +} // Vacation Response // @@ -3831,6 +3903,11 @@ type VacationResponse struct { HtmlBody string `json:"htmlBody,omitempty"` } +var _ Idable = &VacationResponse{} + +func (f VacationResponse) GetObjectType() ObjectType { return VacationResponseType } +func (f VacationResponse) GetId() string { return f.Id } + type VacationResponseGetResponse struct { // The identifier of the account this response pertains to. AccountId string `json:"accountId"` @@ -3849,10 +3926,11 @@ type VacationResponseGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &VacationResponseGetResponse{} +var _ GetResponse[VacationResponse] = &VacationResponseGetResponse{} -func (r VacationResponseGetResponse) GetState() State { return r.State } -func (r VacationResponseGetResponse) GetNotFound() []string { return r.NotFound } +func (r VacationResponseGetResponse) GetState() State { return r.State } +func (r VacationResponseGetResponse) GetNotFound() []string { return r.NotFound } +func (r VacationResponseGetResponse) GetList() []VacationResponse { return r.List } type VacationResponseSetCommand struct { AccountId string `json:"accountId"` @@ -3862,11 +3940,13 @@ type VacationResponseSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &VacationResponseSetCommand{} +var _ SetCommand[VacationResponse] = &VacationResponseSetCommand{} func (c VacationResponseSetCommand) GetCommand() Command { return CommandVacationResponseSet } func (c VacationResponseSetCommand) GetObjectType() ObjectType { return VacationResponseType } -func (c VacationResponseSetCommand) GetResponse() SetResponse { return VacationResponseSetResponse{} } +func (c VacationResponseSetCommand) GetResponse() SetResponse[VacationResponse] { + return VacationResponseSetResponse{} +} type VacationResponseSetResponse struct { AccountId string `json:"accountId"` @@ -3880,13 +3960,14 @@ type VacationResponseSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &VacationResponseSetResponse{} +var _ SetResponse[VacationResponse] = &VacationResponseSetResponse{} func (r VacationResponseSetResponse) GetOldState() State { return r.OldState } func (r VacationResponseSetResponse) GetNewState() State { return r.NewState } func (r VacationResponseSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r VacationResponseSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r VacationResponseSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r VacationResponseSetResponse) GetMarker() VacationResponse { return VacationResponse{} } // One of these attributes must be set, but not both. type DataSourceObject struct { @@ -3932,34 +4013,6 @@ const ( BlobPropertyDigestSha512 = "digest:sha512" ) -type BlobGetCommand struct { - AccountId string `json:"accountId"` - Ids []string `json:"ids,omitempty"` - Properties []string `json:"properties,omitempty"` - Offset int `json:"offset,omitzero"` - Length int `json:"length,omitzero"` -} - -var _ GetCommand = &BlobGetCommand{} - -func (c BlobGetCommand) GetCommand() Command { return CommandBlobGet } -func (c BlobGetCommand) GetObjectType() ObjectType { return BlobType } -func (c BlobGetCommand) GetResponse() GetResponse { return BlobGetResponse{} } - -type BlobGetRefCommand struct { - AccountId string `json:"accountId"` - IdRef *ResultReference `json:"#ids,omitempty"` - Properties []string `json:"properties,omitempty"` - Offset int `json:"offset,omitzero"` - Length int `json:"length,omitzero"` -} - -var _ GetCommand = &BlobGetRefCommand{} - -func (c BlobGetRefCommand) GetCommand() Command { return CommandBlobGet } -func (c BlobGetRefCommand) GetObjectType() ObjectType { return BlobType } -func (c BlobGetRefCommand) GetResponse() GetResponse { return BlobGetResponse{} } - type Blob struct { // The unique identifier of the blob. Id string `json:"id"` @@ -4010,6 +4063,39 @@ func (b *Blob) Digest() string { } } +var _ Idable = &Blob{} + +func (f Blob) GetObjectType() ObjectType { return BlobType } +func (f Blob) GetId() string { return f.Id } + +type BlobGetCommand struct { + AccountId string `json:"accountId"` + Ids []string `json:"ids,omitempty"` + Properties []string `json:"properties,omitempty"` + Offset int `json:"offset,omitzero"` + Length int `json:"length,omitzero"` +} + +var _ GetCommand[Blob] = &BlobGetCommand{} + +func (c BlobGetCommand) GetCommand() Command { return CommandBlobGet } +func (c BlobGetCommand) GetObjectType() ObjectType { return BlobType } +func (c BlobGetCommand) GetResponse() GetResponse[Blob] { return BlobGetResponse{} } + +type BlobGetRefCommand struct { + AccountId string `json:"accountId"` + IdRef *ResultReference `json:"#ids,omitempty"` + Properties []string `json:"properties,omitempty"` + Offset int `json:"offset,omitzero"` + Length int `json:"length,omitzero"` +} + +var _ GetCommand[Blob] = &BlobGetRefCommand{} + +func (c BlobGetRefCommand) GetCommand() Command { return CommandBlobGet } +func (c BlobGetRefCommand) GetObjectType() ObjectType { return BlobType } +func (c BlobGetRefCommand) GetResponse() GetResponse[Blob] { return BlobGetResponse{} } + type BlobGetResponse struct { // The id of the account used for the call. AccountId string `json:"accountId"` @@ -4040,10 +4126,11 @@ type BlobGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &BlobGetResponse{} +var _ GetResponse[Blob] = &BlobGetResponse{} func (r BlobGetResponse) GetState() State { return r.State } func (r BlobGetResponse) GetNotFound() []string { return r.NotFound } +func (r BlobGetResponse) GetList() []Blob { return r.List } type BlobDownload struct { Body io.ReadCloser @@ -4095,6 +4182,10 @@ type SearchSnippet struct { Preview string `json:"preview,omitempty"` } +var _ Foo = &SearchSnippet{} + +func (f SearchSnippet) GetObjectType() ObjectType { return SearchSnippetType } + type SearchSnippetGetRefCommand struct { // The id of the account to use. AccountId string `json:"accountId"` @@ -4228,6 +4319,335 @@ type AddressBook struct { MyRights AddressBookRights `json:"myRights"` } +var _ Idable = &AddressBook{} + +func (f AddressBook) GetObjectType() ObjectType { return AddressBookType } +func (f AddressBook) GetId() string { return f.Id } + +// A ContactCard object contains information about a person, company, or other entity, or represents a group of such entities. +// +// It is a JSCard (JSContact) object, as defined in [RFC9553], with two additional properties. +// +// A contact card with a `kind` property equal to `group` represents a group of contacts. +// Clients often present these separately from other contact cards. +// +// The `members` property, as defined in RFC XXX, Section XXX, contains a set of UIDs for other contacts that are the members +// of this group. +// Clients should consider the group to contain any `ContactCard` with a matching UID, from any account they have access to with +// support for the `urn:ietf:params:jmap:contacts` capability. +// UIDs that cannot be found SHOULD be ignored but preserved. +// +// For example, suppose a user adds contacts from a shared address book to their private group, then temporarily loses access to +// this address book. +// The UIDs cannot be resolved so the contacts will disappear from the group. +// However, if they are given permission to access the data again the UIDs will be found and the contacts will reappear. +type ContactCard struct { + // The id of the Card (immutable; server-set). + // + // The id uniquely identifies a Card with a particular “uid” within a particular account. + // + // This is a JMAP extension and not part of [RFC9553]. + Id string `json:"id,omitempty" doc:"!request,req"` + + // The set of AddressBook ids this Card belongs to. + // + // A card MUST belong to at least one AddressBook at all times (until it is destroyed). + // + // The set is represented as an object, with each key being an AddressBook id. + // + // The value for each key in the object MUST be true. + // + // This is a JMAP extension and not part of [RFC9553]. + AddressBookIds map[string]bool `json:"addressBookIds,omitempty"` + + // The JSContact type of the Card object: the value MUST be "Card". + Type jscontact.TypeOfContactCard `json:"@type,omitempty"` + + // The JSContact version of this Card. + // + // The value MUST be one of the IANA-registered JSContact Version values for the version property. + Version jscontact.JSContactVersion `json:"version"` + + // The date and time when the Card was created (UTCDateTime). + Created time.Time `json:"created,omitzero"` + + // The kind of the entity the Card represents (default: `individual`). + // + // Values are: + // * `individual`: a single person + // * `group`: a group of people or entities + // * `org`: an organization + // * `location`: a named location + // * `device`: a device such as an appliance, a computer, or a network element + // * `application`: a software application + Kind jscontact.ContactCardKind `json:"kind,omitempty"` + + // The language tag, as defined in [RFC5646]. + // + // The language tag that best describes the language used for text in the Card, optionally including + // additional information such as the script. + // + // Note that values MAY be localized in the `localizations` property. + Language string `json:"language,omitempty"` + + // The set of Cards that are members of this group Card. + // + // Each key in the set is the uid property value of the member, and each boolean value MUST be `true`. + // If this property is set, then the value of the kind property MUST be `group`. + // + // The opposite is not true. A group Card will usually contain the members property to specify the members + // of the group, but it is not required to. + // + // A group Card without the members property can be considered an abstract grouping or one whose members + // are known empirically (e.g., `IETF Participants`). + Members map[string]bool `json:"members,omitempty"` + + // The identifier for the product that created the Card. + // + // If set, the value MUST be at least one character long. + ProdId string `json:"prodId,omitempty"` + + // The set of Card objects that relate to the Card. + // + // The value is a map, where each key is the uid property value of the related Card, and the value + // defines the relation + // + // ```json + // { + // "relatedTo": { + // "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": { + // "relation": {"friend": true} + // }, + // "8cacdfb7d1ffdb59@example.com": { + // "relation": {} + // } + // } + // } + // ``` + RelatedTo map[string]jscontact.Relation `json:"relatedTo,omitempty"` + + // An identifier that associates the object as the same across different systems, address books, and views. + // + // The value SHOULD be a URN [RFC8141], but for compatibility with [RFC6350], it MAY also be a URI [RFC3986] + // or free-text value. + // + // The value of the URN SHOULD be in the "uuid" namespace [RFC9562]. + // + // [RFC9562] describes multiple versions of Universally Unique IDentifiers (UUIDs); UUID version 4 is RECOMMENDED. + Uid string `json:"uid,omitempty"` + + // The date and time when the data in the Card was last modified (UTCDateTime). + Updated time.Time `json:"updated,omitzero"` + + // The name of the entity represented by the Card. + // + // This can be any type of name, e.g., it can, but need not, be the legal name of a person. + Name *jscontact.Name `json:"name,omitempty"` + + // The nicknames of the entity represented by the Card. + Nicknames map[string]jscontact.Nickname `json:"nicknames,omitempty"` + + // The company or organization names and units associated with the Card. + Organizations map[string]jscontact.Organization `json:"organizations,omitempty"` + + // The information that directs how to address, speak to, or refer to the entity that is represented by the Card. + SpeakToAs *jscontact.SpeakToAs `json:"speakToAs,omitempty"` + + // The job titles or functional positions of the entity represented by the Card. + Titles map[string]jscontact.Title `json:"titles,omitempty"` + + // The email addresses in which to contact the entity represented by the Card. + Emails map[string]jscontact.EmailAddress `json:"emails,omitempty"` + + // The online services that are associated with the entity represented by the Card. + // + // This can be messaging services, social media profiles, and other. + OnlineServices map[string]jscontact.OnlineService `json:"onlineServices,omitempty"` + + // The phone numbers by which to contact the entity represented by the Card. + Phones map[string]jscontact.Phone `json:"phones,omitempty"` + + // The preferred languages for contacting the entity associated with the Card. + PreferredLanguages map[string]jscontact.LanguagePref `json:"preferredLanguages,omitempty"` + + // The calendaring resources of the entity represented by the Card, such as to look up free-busy information. + // + // A Calendar object has all properties of the Resource data type, with the following additional definitions: + // * The `@type` property value MUST be `Calendar`, if set + // * The `kind` property is mandatory. Its enumerated values are: + // * `calendar`: The resource is a calendar that contains entries such as calendar events or tasks + // * `freeBusy`: The resource allows for free-busy lookups, for example, to schedule group events + Calendars map[string]jscontact.Calendar `json:"calendars,omitempty"` + + // The scheduling addresses by which the entity may receive calendar scheduling invitations. + SchedulingAddresses map[string]jscontact.SchedulingAddress `json:"schedulingAddresses,omitempty"` + + // The addresses of the entity represented by the Card, such as postal addresses or geographic locations. + Addresses map[string]jscontact.Address `json:"addresses,omitempty"` + + // The cryptographic resources such as public keys and certificates associated with the entity represented by the Card. + // + // A CryptoKey object has all properties of the `Resource` data type, with the following additional definition: + // the `@type` property value MUST be `CryptoKey`, if set. + // + // The following example shows how to refer to an external cryptographic resource: + // ```json + // "cryptoKeys": { + // "mykey1": { + // "uri": "https://www.example.com/keys/jdoe.cer" + // } + // } + // ``` + CryptoKeys map[string]jscontact.CryptoKey `json:"cryptoKeys,omitempty"` + + // The directories containing information about the entity represented by the Card. + // + // A Directory object has all properties of the `Resource` data type, with the following additional definitions: + // * The `@type` property value MUST be `Directory`, if set + // * The `kind` property is mandatory; tts enumerated values are: + // * `directory`: the resource is a directory service that the entity represented by the Card is a part of; this + // typically is an organizational directory that also contains associated entities, e.g., co-workers and management + // in a company directory + // * `entry`: the resource is a directory entry of the entity represented by the Card; in contrast to the `directory` + // type, this is the specific URI for the entity within a directory + Directories map[string]jscontact.Directory `json:"directories,omitempty"` + + // The links to resources that do not fit any of the other use-case-specific resource properties. + // + // A Link object has all properties of the `Resource` data type, with the following additional definitions: + // * The `@type` property value MUST be `Link`, if set + // * The `kind` property is optional; tts enumerated values are: + // * `contact`: the resource is a URI by which the entity represented by the Card may be contacted; + // this includes web forms or other media that require user interaction + Links map[string]jscontact.Link `json:"links,omitempty"` + + // The media resources such as photographs, avatars, or sounds that are associated with the entity represented by the Card. + // + // A Media object has all properties of the Resource data type, with the following additional definitions: + // * the `@type` property value MUST be `Media`, if set + // * the `kind` property is mandatory; its enumerated values are: + // * `photo`: the resource is a photograph or avatar + // * `sound`: the resource is audio media, e.g., to specify the proper pronunciation of the name property contents + // * `logo`: the resource is a graphic image or logo associated with the entity represented by the Card + Media map[string]jscontact.Media `json:"media,omitempty"` + + // The property values localized to languages other than the main `language` of the Card. + // + // Localizations provide language-specific alternatives for existing property values and SHOULD NOT add new properties. + // + // The keys in the localizations property value are language tags [RFC5646]; the values are of type `PatchObject` and + // localize the Card in that language tag. + // + // The paths in the `PatchObject` are relative to the Card that includes the localizations property. + // + // A patch MUST NOT target the localizations property. + // + // Conceptually, a Card is localized as follows: + // * Determine the language tag in which the Card should be localized. + // * If the localizations property includes a key for that language, obtain the PatchObject value; + // if there is no such key, stop. + // * Create a copy of the Card, but do not copy the localizations property. + // * Apply all patches in the PatchObject to the copy of the Card. + // * Optionally, set the language property in the copy of the Card. + // * Use the patched copy of the Card as the localized variant of the original Card. + // + // A patch in the `PatchObject` may contain any value type. + // + // Its value MUST be a valid value according to the definition of the patched property. + Localizations map[string]jscontact.PatchObject `json:"localizations,omitempty"` + + // The memorable dates and events for the entity represented by the Card. + Anniversaries map[string]jscontact.Anniversary `json:"anniversaries,omitempty"` + + // The set of free-text keywords, also known as tags. + // + // Each key in the set is a keyword, and each boolean value MUST be `true`. + Keywords map[string]bool `json:"keywords,omitempty"` + + // The free-text notes that are associated with the Card. + Notes map[string]jscontact.Note `json:"notes,omitempty"` + + // The personal information of the entity represented by the Card. + PersonalInfo map[string]jscontact.PersonalInfo `json:"personalInfo,omitempty"` +} + +var _ Foo = &ContactCard{} + +func (f ContactCard) GetObjectType() ObjectType { return ContactCardType } +func (f ContactCard) GetId() string { return f.Id } + +const ( + ContactCardPropertyId = "id" + ContactCardPropertyAddressBookIds = "addressBookIds" + ContactCardPropertyType = "@type" + ContactCardPropertyVersion = "version" + ContactCardPropertyCreated = "created" + ContactCardPropertyKind = "kind" + ContactCardPropertyLanguage = "language" + ContactCardPropertyMembers = "members" + ContactCardPropertyProdId = "prodId" + ContactCardPropertyRelatedTo = "relatedTo" + ContactCardPropertyUid = "uid" + ContactCardPropertyUpdated = "updated" + ContactCardPropertyName = "name" + ContactCardPropertyNicknames = "nicknames" + ContactCardPropertyOrganizations = "organizations" + ContactCardPropertySpeakToAs = "speakToAs" + ContactCardPropertyTitles = "titles" + ContactCardPropertyEmails = "emails" + ContactCardPropertyOnlineServices = "onlineServices" + ContactCardPropertyPhones = "phones" + ContactCardPropertyPreferredLanguages = "preferredLanguages" + ContactCardPropertyCalendars = "calendars" + ContactCardPropertySchedulingAddresses = "schedulingAddresses" + ContactCardPropertyAddresses = "addresses" + ContactCardPropertyCryptoKeys = "cryptoKeys" + ContactCardPropertyDirectories = "directories" + ContactCardPropertyLinks = "links" + ContactCardPropertyMedia = "media" + ContactCardPropertyLocalizations = "localizations" + ContactCardPropertyAnniversaries = "anniversaries" + ContactCardPropertyKeywords = "keywords" + ContactCardPropertyNotes = "notes" + ContactCardPropertyPersonalInfo = "personalInfo" +) + +var ContactCardProperties = []string{ + ContactCardPropertyId, + ContactCardPropertyAddressBookIds, + ContactCardPropertyType, + ContactCardPropertyVersion, + ContactCardPropertyCreated, + ContactCardPropertyKind, + ContactCardPropertyLanguage, + ContactCardPropertyMembers, + ContactCardPropertyProdId, + ContactCardPropertyRelatedTo, + ContactCardPropertyUid, + ContactCardPropertyUpdated, + ContactCardPropertyName, + ContactCardPropertyNicknames, + ContactCardPropertyOrganizations, + ContactCardPropertySpeakToAs, + ContactCardPropertyTitles, + ContactCardPropertyEmails, + ContactCardPropertyOnlineServices, + ContactCardPropertyPhones, + ContactCardPropertyPreferredLanguages, + ContactCardPropertyCalendars, + ContactCardPropertySchedulingAddresses, + ContactCardPropertyAddresses, + ContactCardPropertyCryptoKeys, + ContactCardPropertyDirectories, + ContactCardPropertyLinks, + ContactCardPropertyMedia, + ContactCardPropertyLocalizations, + ContactCardPropertyAnniversaries, + ContactCardPropertyKeywords, + ContactCardPropertyNotes, + ContactCardPropertyPersonalInfo, +} + type CalendarRights struct { // The user may read the free-busy information for this calendar. MayReadFreeBusy bool `json:"mayReadFreeBusy"` @@ -4441,15 +4861,20 @@ type Calendar struct { MyRights *CalendarRights `json:"myRights,omitempty"` } +var _ Idable = &Calendar{} + +func (f Calendar) GetObjectType() ObjectType { return CalendarType } +func (f Calendar) GetId() string { return f.Id } + type CalendarChange struct { // The user-visible name of the calendar. // // This may be any UTF-8 string of at least 1 character in length and maximum 255 octets in size. - Name string `json:"name"` + Name *string `json:"name"` // An optional longer-form description of the calendar, to provide context in shared environments // where users need more than just the name. - Description string `json:"description,omitempty"` + Description *string `json:"description,omitempty"` // A color to be used when displaying events associated with the calendar. // @@ -4458,7 +4883,7 @@ type CalendarChange struct { // notation, as defined in Section 4.2.1 of CSS Color Module Level 3. // // The color SHOULD have sufficient contrast to be used as text on a white background. - Color string `json:"color,omitempty"` + Color *string `json:"color,omitempty"` // Defines the sort order of calendars when presented in the client’s UI, so it is consistent // between devices. @@ -4471,7 +4896,7 @@ type CalendarChange struct { // Calendars with equal order SHOULD be sorted in alphabetical order by name. // // The sorting should take into account locale-specific character order convention. - SortOrder uint `json:"sortOrder,omitzero"` + SortOrder *uint `json:"sortOrder,omitzero"` // True if the user has indicated they wish to see this Calendar in their client. // @@ -4484,7 +4909,7 @@ type CalendarChange struct { // For example, a company may have a large number of shared calendars which all employees have // permission to access, but you would only subscribe to the ones you care about and want to be able // to have normally accessible. - IsSubscribed bool `json:"isSubscribed"` + IsSubscribed *bool `json:"isSubscribed"` // Should the calendar’s events be displayed to the user at the moment? // @@ -4492,18 +4917,7 @@ type CalendarChange struct { // // If an event is in multiple calendars, it should be displayed if `isVisible` is `true` // for any of those calendars. - IsVisible bool `json:"isVisible" default:"true" doc:"opt"` - - // This SHOULD be true for exactly one calendar in any account, and MUST NOT be true for more - // than one calendar within an account (server-set). - // - // The default calendar should be used by clients whenever they need to choose a calendar - // for the user within this account, and they do not have any other information on which to make - // a choice. - // - // For example, if the user creates a new event, the client may automatically set the event as - // belonging to the default calendar from the user’s primary account. - IsDefault bool `json:"isDefault,omitzero"` + IsVisible *bool `json:"isVisible" default:"true" doc:"opt"` // Should the calendar’s events be used as part of availability calculation? // @@ -4513,7 +4927,7 @@ type CalendarChange struct { // * `none`: all events are ignored (but may be considered if also in another calendar). // // This should default to “all” for the calendars in the user’s own account, and “none” for calendars shared with the user. - IncludeInAvailability IncludeInAvailability `json:"includeInAvailability,omitempty"` + IncludeInAvailability *IncludeInAvailability `json:"includeInAvailability,omitempty"` // A map of alert ids to Alert objects (see [@!RFC8984], Section 4.5.2) to apply for events // where `showWithoutTime` is `false` and `useDefaultAlerts` is `true`. @@ -4551,7 +4965,7 @@ type CalendarChange struct { // If null, the `timeZone` of the account’s associated `Principal` will be used. // // Clients SHOULD use this as the default for new events in this calendar if set. - TimeZone string `json:"timeZone,omitempty"` + TimeZone *string `json:"timeZone,omitempty"` // A map of `Principal` id to rights for principals this calendar is shared with. // @@ -4576,6 +4990,39 @@ type CalendarChange struct { MyRights *CalendarRights `json:"myRights,omitempty"` } +var _ Change = CalendarChange{} + +func (a CalendarChange) AsPatch() PatchObject { + p := PatchObject{} + if a.Name != nil { + p["name"] = *a.Name + } + if a.Description != nil { + p["description"] = *a.Description + } + if a.Color != nil { + p["color"] = *a.Color + } + if a.SortOrder != nil { + p["sortOrder"] = *a.SortOrder + } + if a.IsSubscribed != nil { + p["isSubscribed"] = *a.IsSubscribed + } + if a.IsVisible != nil { + p["isVisible"] = *a.IsVisible + } + if a.IncludeInAvailability != nil { + p["includeInAvailability"] = *a.IncludeInAvailability + } + // TODO DefaultAlertsWithTime + // TODO DefaultAlertsWithoutTime + // TODO TimeZone + // TODO ShareWith + // TODO MyRights + return p +} + // A CalendarEvent object contains information about an event, or recurring series of events, // that takes place at a particular time. // @@ -4659,6 +5106,11 @@ type CalendarEvent struct { jscalendar.Event } +var _ Idable = &CalendarEvent{} + +func (f CalendarEvent) GetObjectType() ObjectType { return CalendarEventType } +func (f CalendarEvent) GetId() string { return f.Id } + const ( CalendarEventPropertyId = "id" CalendarEventPropertyBaseEventId = "baseEventId" @@ -5380,6 +5832,11 @@ type Principal struct { Accounts map[string]Account `json:"accounts,omitempty"` } +var _ Idable = &Principal{} + +func (f Principal) GetObjectType() ObjectType { return PrincipalType } +func (f Principal) GetId() string { return f.Id } + type ShareChangePerson struct { // The name of the person who made the change. Name string `json:"name"` @@ -5418,29 +5875,6 @@ type ShareNotification struct { NewRights map[string]bool `json:"newRights,omitempty"` } -// TODO unused -type Shareable struct { - // Has the user indicated they wish to see this data? - // - // The initial value for this when data is shared by another user is implementation dependent, - // although data types may give advice on appropriate defaults. - IsSubscribed bool `json:"isSubscribed,omitzero"` - - // The set of permissions the user currently has. - // - // Appropriate permissions are domain specific and must be defined per data type. - MyRights map[string]bool `json:"myRights,omitempty"` - - // A map of principal id to rights to give that principal, or null if not shared with anyone. - // - // The account id for the principal id can be found in the capabilities of the `Account` this object is in. - // - // Users with appropriate permission may set this property to modify who the data is shared with. - // - // The principal that owns the account this data is in MUST NOT be in the set of sharees; their rights are implicit. - ShareWith map[string]map[string]bool `json:"shareWith,omitempty"` -} - // The Quota is an object that displays the limit set to an account usage. // // It then shows as well the current usage in regard to that limit. @@ -5518,6 +5952,11 @@ type Quota struct { Description string `json:"description,omitempty"` } +var _ Idable = &Quota{} + +func (f Quota) GetObjectType() ObjectType { return QuotaType } +func (f Quota) GetId() string { return f.Id } + // See [RFC8098] for the exact meaning of these different fields. // // These fields are defined as case insensitive in [RFC8098] but are case sensitive in this RFC @@ -5613,11 +6052,11 @@ type QuotaGetCommand struct { Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &QuotaGetCommand{} +var _ GetCommand[Quota] = &QuotaGetCommand{} -func (c QuotaGetCommand) GetCommand() Command { return CommandQuotaGet } -func (c QuotaGetCommand) GetObjectType() ObjectType { return QuotaType } -func (c QuotaGetCommand) GetResponse() GetResponse { return QuotaGetResponse{} } +func (c QuotaGetCommand) GetCommand() Command { return CommandQuotaGet } +func (c QuotaGetCommand) GetObjectType() ObjectType { return QuotaType } +func (c QuotaGetCommand) GetResponse() GetResponse[Quota] { return QuotaGetResponse{} } type QuotaGetRefCommand struct { AccountId string `json:"accountId"` @@ -5625,11 +6064,11 @@ type QuotaGetRefCommand struct { PropertiesRef *ResultReference `json:"#properties,omitempty"` } -var _ GetCommand = &QuotaGetRefCommand{} +var _ GetCommand[Quota] = &QuotaGetRefCommand{} -func (c QuotaGetRefCommand) GetCommand() Command { return CommandQuotaGet } -func (c QuotaGetRefCommand) GetObjectType() ObjectType { return QuotaType } -func (c QuotaGetRefCommand) GetResponse() GetResponse { return QuotaGetResponse{} } +func (c QuotaGetRefCommand) GetCommand() Command { return CommandQuotaGet } +func (c QuotaGetRefCommand) GetObjectType() ObjectType { return QuotaType } +func (c QuotaGetRefCommand) GetResponse() GetResponse[Quota] { return QuotaGetResponse{} } type QuotaGetResponse struct { AccountId string `json:"accountId"` @@ -5638,10 +6077,11 @@ type QuotaGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &QuotaGetResponse{} +var _ GetResponse[Quota] = &QuotaGetResponse{} func (r QuotaGetResponse) GetState() State { return r.State } func (r QuotaGetResponse) GetNotFound() []string { return r.NotFound } +func (r QuotaGetResponse) GetList() []Quota { return r.List } type QuotaChangesCommand struct { // The id of the account to use. @@ -5665,11 +6105,11 @@ type QuotaChangesCommand struct { UpdatedProperties []string `json:"updatedProperties,omitempty"` } -var _ ChangesCommand = &QuotaChangesCommand{} +var _ ChangesCommand[Quota] = &QuotaChangesCommand{} -func (c QuotaChangesCommand) GetCommand() Command { return CommandQuotaChanges } -func (c QuotaChangesCommand) GetObjectType() ObjectType { return QuotaType } -func (c QuotaChangesCommand) GetResponse() ChangesResponse { return QuotaChangesResponse{} } +func (c QuotaChangesCommand) GetCommand() Command { return CommandQuotaChanges } +func (c QuotaChangesCommand) GetObjectType() ObjectType { return QuotaType } +func (c QuotaChangesCommand) GetResponse() ChangesResponse[Quota] { return QuotaChangesResponse{} } type QuotaChangesResponse struct { // The id of the account used for the call. @@ -5695,7 +6135,7 @@ type QuotaChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &QuotaChangesResponse{} +var _ ChangesResponse[Quota] = &QuotaChangesResponse{} func (r QuotaChangesResponse) GetOldState() State { return r.OldState } func (r QuotaChangesResponse) GetNewState() State { return r.NewState } @@ -5703,28 +6143,33 @@ func (r QuotaChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges func (r QuotaChangesResponse) GetCreated() []string { return r.Created } func (r QuotaChangesResponse) GetUpdated() []string { return r.Updated } func (r QuotaChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r QuotaChangesResponse) GetMarker() Quota { return Quota{} } type AddressBookGetCommand struct { AccountId string `json:"accountId"` Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &AddressBookGetCommand{} +var _ GetCommand[AddressBook] = &AddressBookGetCommand{} func (c AddressBookGetCommand) GetCommand() Command { return CommandAddressBookGet } func (c AddressBookGetCommand) GetObjectType() ObjectType { return AddressBookType } -func (c AddressBookGetCommand) GetResponse() GetResponse { return AddressBookGetResponse{} } +func (c AddressBookGetCommand) GetResponse() GetResponse[AddressBook] { + return AddressBookGetResponse{} +} type AddressBookGetRefCommand struct { AccountId string `json:"accountId"` IdsRef *ResultReference `json:"#ids,omitempty"` } -var _ GetCommand = &AddressBookGetRefCommand{} +var _ GetCommand[AddressBook] = &AddressBookGetRefCommand{} func (c AddressBookGetRefCommand) GetCommand() Command { return CommandAddressBookGet } func (c AddressBookGetRefCommand) GetObjectType() ObjectType { return AddressBookType } -func (c AddressBookGetRefCommand) GetResponse() GetResponse { return AddressBookGetResponse{} } +func (c AddressBookGetRefCommand) GetResponse() GetResponse[AddressBook] { + return AddressBookGetResponse{} +} type AddressBookGetResponse struct { AccountId string `json:"accountId"` @@ -5733,10 +6178,11 @@ type AddressBookGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &AddressBookGetResponse{} +var _ GetResponse[AddressBook] = &AddressBookGetResponse{} -func (r AddressBookGetResponse) GetState() State { return r.State } -func (r AddressBookGetResponse) GetNotFound() []string { return r.NotFound } +func (r AddressBookGetResponse) GetState() State { return r.State } +func (r AddressBookGetResponse) GetNotFound() []string { return r.NotFound } +func (r AddressBookGetResponse) GetList() []AddressBook { return r.List } type AddressBookChange struct { // The user-visible name of the AddressBook. @@ -5809,11 +6255,13 @@ type AddressBookSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &AddressBookSetCommand{} +var _ SetCommand[AddressBook] = &AddressBookSetCommand{} func (c AddressBookSetCommand) GetCommand() Command { return CommandAddressBookSet } func (c AddressBookSetCommand) GetObjectType() ObjectType { return AddressBookType } -func (c AddressBookSetCommand) GetResponse() SetResponse { return AddressBookSetResponse{} } +func (c AddressBookSetCommand) GetResponse() SetResponse[AddressBook] { + return AddressBookSetResponse{} +} type AddressBookSetResponse struct { // The id of the account used for the call. @@ -5862,13 +6310,14 @@ type AddressBookSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &AddressBookSetResponse{} +var _ SetResponse[AddressBook] = &AddressBookSetResponse{} func (r AddressBookSetResponse) GetOldState() State { return r.OldState } func (r AddressBookSetResponse) GetNewState() State { return r.NewState } func (r AddressBookSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r AddressBookSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r AddressBookSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r AddressBookSetResponse) GetMarker() AddressBook { return AddressBook{} } type AddressBookChangesCommand struct { // The id of the account to use. @@ -5893,11 +6342,13 @@ type AddressBookChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &AddressBookChangesCommand{} +var _ ChangesCommand[AddressBook] = &AddressBookChangesCommand{} -func (c AddressBookChangesCommand) GetCommand() Command { return CommandAddressBookChanges } -func (c AddressBookChangesCommand) GetObjectType() ObjectType { return AddressBookType } -func (c AddressBookChangesCommand) GetResponse() ChangesResponse { return AddressBookChangesResponse{} } +func (c AddressBookChangesCommand) GetCommand() Command { return CommandAddressBookChanges } +func (c AddressBookChangesCommand) GetObjectType() ObjectType { return AddressBookType } +func (c AddressBookChangesCommand) GetResponse() ChangesResponse[AddressBook] { + return AddressBookChangesResponse{} +} type AddressBookChangesResponse struct { // The id of the account used for the call. @@ -5924,7 +6375,7 @@ type AddressBookChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &AddressBookChangesResponse{} +var _ ChangesResponse[AddressBook] = &AddressBookChangesResponse{} func (r AddressBookChangesResponse) GetOldState() State { return r.OldState } func (r AddressBookChangesResponse) GetNewState() State { return r.NewState } @@ -5932,6 +6383,7 @@ func (r AddressBookChangesResponse) GetHasMoreChanges() bool { return r.HasMoreC func (r AddressBookChangesResponse) GetCreated() []string { return r.Created } func (r AddressBookChangesResponse) GetUpdated() []string { return r.Updated } func (r AddressBookChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r AddressBookChangesResponse) GetMarker() AddressBook { return AddressBook{} } type ContactCardComparator struct { // The name of the property on the objects to compare. @@ -6250,11 +6702,13 @@ type ContactCardGetCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &ContactCardGetCommand{} +var _ GetCommand[ContactCard] = &ContactCardGetCommand{} func (c ContactCardGetCommand) GetCommand() Command { return CommandContactCardGet } func (c ContactCardGetCommand) GetObjectType() ObjectType { return ContactCardType } -func (c ContactCardGetCommand) GetResponse() GetResponse { return ContactCardGetResponse{} } +func (c ContactCardGetCommand) GetResponse() GetResponse[ContactCard] { + return ContactCardGetResponse{} +} type ContactCardGetRefCommand struct { // The ids of the ContactCard objects to return. @@ -6274,11 +6728,13 @@ type ContactCardGetRefCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &ContactCardGetRefCommand{} +var _ GetCommand[ContactCard] = &ContactCardGetRefCommand{} func (c ContactCardGetRefCommand) GetCommand() Command { return CommandContactCardGet } func (c ContactCardGetRefCommand) GetObjectType() ObjectType { return ContactCardType } -func (c ContactCardGetRefCommand) GetResponse() GetResponse { return ContactCardGetResponse{} } +func (c ContactCardGetRefCommand) GetResponse() GetResponse[ContactCard] { + return ContactCardGetResponse{} +} type ContactCardGetResponse struct { // The id of the account used for the call. @@ -6299,7 +6755,7 @@ type ContactCardGetResponse struct { // // If an identical id is included more than once in the request, the server MUST only include it once in either // the list or the notFound argument of the response. - List []jscontact.ContactCard `json:"list"` + List []ContactCard `json:"list"` // This array contains the ids passed to the method for records that do not exist. // @@ -6307,10 +6763,11 @@ type ContactCardGetResponse struct { NotFound []string `json:"notFound"` } -var _ GetResponse = &ContactCardGetResponse{} +var _ GetResponse[ContactCard] = &ContactCardGetResponse{} -func (r ContactCardGetResponse) GetState() State { return r.State } -func (r ContactCardGetResponse) GetNotFound() []string { return r.NotFound } +func (r ContactCardGetResponse) GetState() State { return r.State } +func (r ContactCardGetResponse) GetNotFound() []string { return r.NotFound } +func (r ContactCardGetResponse) GetList() []ContactCard { return r.List } type ContactCardChangesCommand struct { // The id of the account to use. @@ -6329,11 +6786,13 @@ type ContactCardChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitempty"` } -var _ ChangesCommand = &ContactCardChangesCommand{} +var _ ChangesCommand[ContactCard] = &ContactCardChangesCommand{} -func (c ContactCardChangesCommand) GetCommand() Command { return CommandContactCardChanges } -func (c ContactCardChangesCommand) GetObjectType() ObjectType { return ContactCardType } -func (c ContactCardChangesCommand) GetResponse() ChangesResponse { return ContactCardChangesResponse{} } +func (c ContactCardChangesCommand) GetCommand() Command { return CommandContactCardChanges } +func (c ContactCardChangesCommand) GetObjectType() ObjectType { return ContactCardType } +func (c ContactCardChangesCommand) GetResponse() ChangesResponse[ContactCard] { + return ContactCardChangesResponse{} +} type ContactCardChangesResponse struct { // The id of the account used for the call. @@ -6359,7 +6818,7 @@ type ContactCardChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &ContactCardChangesResponse{} +var _ ChangesResponse[ContactCard] = &ContactCardChangesResponse{} func (r ContactCardChangesResponse) GetOldState() State { return r.OldState } func (r ContactCardChangesResponse) GetNewState() State { return r.NewState } @@ -6367,6 +6826,7 @@ func (r ContactCardChangesResponse) GetHasMoreChanges() bool { return r.HasMoreC func (r ContactCardChangesResponse) GetCreated() []string { return r.Created } func (r ContactCardChangesResponse) GetUpdated() []string { return r.Updated } func (r ContactCardChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r ContactCardChangesResponse) GetMarker() ContactCard { return ContactCard{} } type ContactCardUpdate map[string]any @@ -6390,7 +6850,7 @@ type ContactCardSetCommand struct { // Any such property may be omitted by the client. // // The client MUST omit any properties that may only be set by the server. - Create map[string]jscontact.ContactCard `json:"create,omitempty"` + Create map[string]ContactCard `json:"create,omitempty"` // A map of an id to a `Patch` object to apply to the current Email object with that id, // or null if no objects are to be updated. @@ -6423,11 +6883,13 @@ type ContactCardSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &ContactCardSetCommand{} +var _ SetCommand[ContactCard] = &ContactCardSetCommand{} func (c ContactCardSetCommand) GetCommand() Command { return CommandContactCardSet } func (c ContactCardSetCommand) GetObjectType() ObjectType { return ContactCardType } -func (c ContactCardSetCommand) GetResponse() SetResponse { return ContactCardSetResponse{} } +func (c ContactCardSetCommand) GetResponse() SetResponse[ContactCard] { + return ContactCardSetResponse{} +} type ContactCardSetResponse struct { // The id of the account used for the call. @@ -6448,7 +6910,7 @@ type ContactCardSetResponse struct { // that were omitted by the client and thus set to a default by the server. // // This argument is null if no ContactCard objects were successfully created. - Created map[string]*jscontact.ContactCard `json:"created,omitempty"` + Created map[string]*ContactCard `json:"created,omitempty"` // The keys in this map are the ids of all Emails that were successfully updated. // @@ -6458,7 +6920,7 @@ type ContactCardSetResponse struct { // This lets the client know of any changes to server-set or computed properties. // // This argument is null if no ContactCard objects were successfully updated. - Updated map[string]*jscontact.ContactCard `json:"updated,omitempty"` + Updated map[string]*ContactCard `json:"updated,omitempty"` // A list of ContactCard ids for records that were successfully destroyed, or null if none. Destroyed []string `json:"destroyed,omitempty"` @@ -6476,13 +6938,14 @@ type ContactCardSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &ContactCardSetResponse{} +var _ SetResponse[ContactCard] = &ContactCardSetResponse{} func (r ContactCardSetResponse) GetOldState() State { return r.OldState } func (r ContactCardSetResponse) GetNewState() State { return r.NewState } func (r ContactCardSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r ContactCardSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r ContactCardSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r ContactCardSetResponse) GetMarker() ContactCard { return ContactCard{} } type CalendarEventParseCommand struct { // The id of the account to use. @@ -6523,22 +6986,22 @@ type CalendarGetCommand struct { Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &CalendarGetCommand{} +var _ GetCommand[Calendar] = &CalendarGetCommand{} -func (c CalendarGetCommand) GetCommand() Command { return CommandCalendarGet } -func (c CalendarGetCommand) GetObjectType() ObjectType { return CalendarType } -func (c CalendarGetCommand) GetResponse() GetResponse { return CalendarGetResponse{} } +func (c CalendarGetCommand) GetCommand() Command { return CommandCalendarGet } +func (c CalendarGetCommand) GetObjectType() ObjectType { return CalendarType } +func (c CalendarGetCommand) GetResponse() GetResponse[Calendar] { return CalendarGetResponse{} } type CalendarGetRefCommand struct { AccountId string `json:"accountId"` IdsRef *ResultReference `json:"#ids,omitempty"` } -var _ GetCommand = &CalendarGetRefCommand{} +var _ GetCommand[Calendar] = &CalendarGetRefCommand{} -func (c CalendarGetRefCommand) GetCommand() Command { return CommandCalendarGet } -func (c CalendarGetRefCommand) GetObjectType() ObjectType { return CalendarType } -func (c CalendarGetRefCommand) GetResponse() GetResponse { return CalendarGetResponse{} } +func (c CalendarGetRefCommand) GetCommand() Command { return CommandCalendarGet } +func (c CalendarGetRefCommand) GetObjectType() ObjectType { return CalendarType } +func (c CalendarGetRefCommand) GetResponse() GetResponse[Calendar] { return CalendarGetResponse{} } type CalendarGetResponse struct { AccountId string `json:"accountId"` @@ -6547,10 +7010,11 @@ type CalendarGetResponse struct { NotFound []string `json:"notFound,omitempty"` } -var _ GetResponse = &CalendarGetResponse{} +var _ GetResponse[Calendar] = &CalendarGetResponse{} func (r CalendarGetResponse) GetState() State { return r.State } func (r CalendarGetResponse) GetNotFound() []string { return r.NotFound } +func (r CalendarGetResponse) GetList() []Calendar { return r.List } type CalendarSetCommand struct { AccountId string `json:"accountId"` @@ -6560,11 +7024,13 @@ type CalendarSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &CalendarSetCommand{} +var _ SetCommand[Calendar] = &CalendarSetCommand{} func (c CalendarSetCommand) GetCommand() Command { return CommandCalendarSet } func (c CalendarSetCommand) GetObjectType() ObjectType { return CalendarType } -func (c CalendarSetCommand) GetResponse() SetResponse { return CalendarSetResponse{} } +func (c CalendarSetCommand) GetResponse() SetResponse[Calendar] { + return CalendarSetResponse{} +} type CalendarSetResponse struct { // The id of the account used for the call. @@ -6613,13 +7079,14 @@ type CalendarSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &CalendarSetResponse{} +var _ SetResponse[Calendar] = &CalendarSetResponse{} func (r CalendarSetResponse) GetOldState() State { return r.OldState } func (r CalendarSetResponse) GetNewState() State { return r.NewState } func (r CalendarSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r CalendarSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r CalendarSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r CalendarSetResponse) GetMarker() Calendar { return Calendar{} } type CalendarChangesCommand struct { // The id of the account to use. @@ -6644,11 +7111,13 @@ type CalendarChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &CalendarChangesCommand{} +var _ ChangesCommand[Calendar] = &CalendarChangesCommand{} -func (c CalendarChangesCommand) GetCommand() Command { return CommandCalendarChanges } -func (c CalendarChangesCommand) GetObjectType() ObjectType { return CalendarType } -func (c CalendarChangesCommand) GetResponse() ChangesResponse { return CalendarChangesResponse{} } +func (c CalendarChangesCommand) GetCommand() Command { return CommandCalendarChanges } +func (c CalendarChangesCommand) GetObjectType() ObjectType { return CalendarType } +func (c CalendarChangesCommand) GetResponse() ChangesResponse[Calendar] { + return CalendarChangesResponse{} +} type CalendarChangesResponse struct { // The id of the account used for the call. @@ -6674,7 +7143,7 @@ type CalendarChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &CalendarChangesResponse{} +var _ ChangesResponse[Calendar] = &CalendarChangesResponse{} func (r CalendarChangesResponse) GetOldState() State { return r.OldState } func (r CalendarChangesResponse) GetNewState() State { return r.NewState } @@ -6682,6 +7151,7 @@ func (r CalendarChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChan func (r CalendarChangesResponse) GetCreated() []string { return r.Created } func (r CalendarChangesResponse) GetUpdated() []string { return r.Updated } func (r CalendarChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r CalendarChangesResponse) GetMarker() Calendar { return Calendar{} } type CalendarEventComparator struct { // The name of the property on the objects to compare. @@ -6952,11 +7422,13 @@ type CalendarEventGetCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &CalendarEventGetCommand{} +var _ GetCommand[CalendarEvent] = &CalendarEventGetCommand{} func (c CalendarEventGetCommand) GetCommand() Command { return CommandCalendarEventGet } func (c CalendarEventGetCommand) GetObjectType() ObjectType { return CalendarEventType } -func (c CalendarEventGetCommand) GetResponse() GetResponse { return CalendarEventGetResponse{} } +func (c CalendarEventGetCommand) GetResponse() GetResponse[CalendarEvent] { + return CalendarEventGetResponse{} +} type CalendarEventGetRefCommand struct { // The ids of the CalendarEvent objects to return. @@ -6976,11 +7448,13 @@ type CalendarEventGetRefCommand struct { Properties []string `json:"properties,omitempty"` } -var _ GetCommand = &CalendarEventGetRefCommand{} +var _ GetCommand[CalendarEvent] = &CalendarEventGetRefCommand{} func (c CalendarEventGetRefCommand) GetCommand() Command { return CommandCalendarEventGet } func (c CalendarEventGetRefCommand) GetObjectType() ObjectType { return CalendarEventType } -func (c CalendarEventGetRefCommand) GetResponse() GetResponse { return CalendarEventGetResponse{} } +func (c CalendarEventGetRefCommand) GetResponse() GetResponse[CalendarEvent] { + return CalendarEventGetResponse{} +} type CalendarEventGetResponse struct { // The id of the account used for the call. @@ -7009,10 +7483,11 @@ type CalendarEventGetResponse struct { NotFound []string `json:"notFound"` } -var _ GetResponse = &CalendarEventGetResponse{} +var _ GetResponse[CalendarEvent] = &CalendarEventGetResponse{} -func (r CalendarEventGetResponse) GetState() State { return r.State } -func (r CalendarEventGetResponse) GetNotFound() []string { return r.NotFound } +func (r CalendarEventGetResponse) GetState() State { return r.State } +func (r CalendarEventGetResponse) GetNotFound() []string { return r.NotFound } +func (r CalendarEventGetResponse) GetList() []CalendarEvent { return r.List } type CalendarEventChangesCommand struct { // The id of the account to use. @@ -7037,11 +7512,13 @@ type CalendarEventChangesCommand struct { MaxChanges *uint `json:"maxChanges,omitzero"` } -var _ ChangesCommand = &CalendarEventChangesCommand{} +var _ ChangesCommand[CalendarEvent] = &CalendarEventChangesCommand{} -func (c CalendarEventChangesCommand) GetCommand() Command { return CommandCalendarEventChanges } -func (c CalendarEventChangesCommand) GetObjectType() ObjectType { return CalendarEventType } -func (c CalendarEventChangesCommand) GetResponse() ChangesResponse { return CalendarChangesResponse{} } +func (c CalendarEventChangesCommand) GetCommand() Command { return CommandCalendarEventChanges } +func (c CalendarEventChangesCommand) GetObjectType() ObjectType { return CalendarEventType } +func (c CalendarEventChangesCommand) GetResponse() ChangesResponse[CalendarEvent] { + return CalendarEventChangesResponse{} +} type CalendarEventChangesResponse struct { // The id of the account used for the call. @@ -7067,14 +7544,15 @@ type CalendarEventChangesResponse struct { Destroyed []string `json:"destroyed,omitempty"` } -var _ ChangesResponse = &CalendarEventChangesResponse{} +var _ ChangesResponse[CalendarEvent] = &CalendarEventChangesResponse{} -func (r CalendarEventChangesResponse) GetOldState() State { return r.OldState } -func (r CalendarEventChangesResponse) GetNewState() State { return r.NewState } -func (r CalendarEventChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges } -func (r CalendarEventChangesResponse) GetCreated() []string { return r.Created } -func (r CalendarEventChangesResponse) GetUpdated() []string { return r.Updated } -func (r CalendarEventChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r CalendarEventChangesResponse) GetOldState() State { return r.OldState } +func (r CalendarEventChangesResponse) GetNewState() State { return r.NewState } +func (r CalendarEventChangesResponse) GetHasMoreChanges() bool { return r.HasMoreChanges } +func (r CalendarEventChangesResponse) GetCreated() []string { return r.Created } +func (r CalendarEventChangesResponse) GetUpdated() []string { return r.Updated } +func (r CalendarEventChangesResponse) GetDestroyed() []string { return r.Destroyed } +func (r CalendarEventChangesResponse) GetMarker() CalendarEvent { return CalendarEvent{} } type CalendarEventUpdate map[string]any @@ -7131,11 +7609,13 @@ type CalendarEventSetCommand struct { Destroy []string `json:"destroy,omitempty"` } -var _ SetCommand = &CalendarEventSetCommand{} +var _ SetCommand[CalendarEvent] = &CalendarEventSetCommand{} func (c CalendarEventSetCommand) GetCommand() Command { return CommandCalendarEventSet } func (c CalendarEventSetCommand) GetObjectType() ObjectType { return CalendarEventType } -func (c CalendarEventSetCommand) GetResponse() SetResponse { return CalendarSetResponse{} } +func (c CalendarEventSetCommand) GetResponse() SetResponse[CalendarEvent] { + return CalendarEventSetResponse{} +} type CalendarEventSetResponse struct { // The id of the account used for the call. @@ -7184,35 +7664,36 @@ type CalendarEventSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -var _ SetResponse = &CalendarEventSetResponse{} +var _ SetResponse[CalendarEvent] = &CalendarEventSetResponse{} func (r CalendarEventSetResponse) GetOldState() State { return r.OldState } func (r CalendarEventSetResponse) GetNewState() State { return r.NewState } func (r CalendarEventSetResponse) GetNotCreated() map[string]SetError { return r.NotCreated } func (r CalendarEventSetResponse) GetNotUpdated() map[string]SetError { return r.NotUpdated } func (r CalendarEventSetResponse) GetNotDestroyed() map[string]SetError { return r.NotDestroyed } +func (r CalendarEventSetResponse) GetMarker() CalendarEvent { return CalendarEvent{} } type PrincipalGetCommand struct { AccountId string `json:"accountId"` Ids []string `json:"ids,omitempty"` } -var _ GetCommand = &PrincipalGetCommand{} +var _ GetCommand[Principal] = &PrincipalGetCommand{} -func (c PrincipalGetCommand) GetCommand() Command { return CommandPrincipalGet } -func (c PrincipalGetCommand) GetObjectType() ObjectType { return PrincipalType } -func (c PrincipalGetCommand) GetResponse() GetResponse { return PrincipalGetResponse{} } +func (c PrincipalGetCommand) GetCommand() Command { return CommandPrincipalGet } +func (c PrincipalGetCommand) GetObjectType() ObjectType { return PrincipalType } +func (c PrincipalGetCommand) GetResponse() GetResponse[Principal] { return PrincipalGetResponse{} } type PrincipalGetRefCommand struct { AccountId string `json:"accountId"` IdsRef *ResultReference `json:"#ids,omitempty"` } -var _ GetCommand = &PrincipalGetRefCommand{} +var _ GetCommand[Principal] = &PrincipalGetRefCommand{} -func (c PrincipalGetRefCommand) GetCommand() Command { return CommandPrincipalGet } -func (c PrincipalGetRefCommand) GetObjectType() ObjectType { return PrincipalType } -func (c PrincipalGetRefCommand) GetResponse() GetResponse { return PrincipalGetResponse{} } +func (c PrincipalGetRefCommand) GetCommand() Command { return CommandPrincipalGet } +func (c PrincipalGetRefCommand) GetObjectType() ObjectType { return PrincipalType } +func (c PrincipalGetRefCommand) GetResponse() GetResponse[Principal] { return PrincipalGetResponse{} } type PrincipalGetResponse struct { // The id of the account used for the call. @@ -7238,10 +7719,12 @@ type PrincipalGetResponse struct { NotFound []string `json:"notFound"` } -var _ GetResponse = &PrincipalGetResponse{} +var _ GetResponse[Principal] = &PrincipalGetResponse{} func (r PrincipalGetResponse) GetState() State { return r.State } func (r PrincipalGetResponse) GetNotFound() []string { return r.NotFound } +func (r PrincipalGetResponse) GetList() []Principal { return r.List } +func (r PrincipalGetResponse) GetMarker() Principal { return Principal{} } type PrincipalFilterElement interface { _isAPrincipalFilterElement() // marker method diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index 42cecb03a2..c3a815529c 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -11,7 +11,6 @@ import ( "time" "github.com/opencloud-eu/opencloud/pkg/jscalendar" - "github.com/opencloud-eu/opencloud/pkg/jscontact" c "github.com/opencloud-eu/opencloud/pkg/jscontact" ) @@ -924,14 +923,6 @@ func (e Exemplar) AddressBookGetResponse() AddressBookGetResponse { } } -func (e Exemplar) AddressBooksResponse() AddressBooksResponse { - a := e.AddressBook() - b, _, _ := e.OtherAddressBook() - return AddressBooksResponse{ - AddressBooks: []AddressBook{a, b}, - } -} - func (e Exemplar) JSContactEmailAddress() c.EmailAddress { return c.EmailAddress{ Type: c.EmailAddressType, @@ -1077,13 +1068,6 @@ func (e Exemplar) Calendar() Calendar { } } -func (e Exemplar) CalendarsResponse() CalendarsResponse { - a := e.Calendar() - return CalendarsResponse{ - Calendars: []Calendar{a}, - } -} - func (e Exemplar) CalendarGetResponse() CalendarGetResponse { a := e.Calendar() return CalendarGetResponse{ @@ -1400,10 +1384,10 @@ func (e Exemplar) PersonalInfo() c.PersonalInfo { } } -func (e Exemplar) DesignContactCard() (c.ContactCard, string, string) { +func (e Exemplar) DesignContactCard() (ContactCard, string, string) { created, _ := time.Parse(time.RFC3339, "2025-07-09T07:12:28+02:00") updated, _ := time.Parse(time.RFC3339, "2025-07-10T09:58:01+02:00") - return c.ContactCard{ + return ContactCard{ Type: c.ContactCardType, Kind: c.ContactCardKindIndividual, Id: "loTh8ahmubei", @@ -1572,10 +1556,10 @@ func (e Exemplar) DesignContactCard() (c.ContactCard, string, string) { }, "Another Contact Card", "other" } -func (e Exemplar) ContactCard() c.ContactCard { +func (e Exemplar) ContactCard() ContactCard { created, _ := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") updated, _ := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00") - return c.ContactCard{ + return ContactCard{ Type: c.ContactCardType, Kind: c.ContactCardKindGroup, Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7", @@ -1876,7 +1860,7 @@ func (e Exemplar) ContactCardGetResponse() ContactCardGetResponse { AccountId: e.AccountId, State: "ewohl8ie", NotFound: []string{"eeaa2"}, - List: []c.ContactCard{a, b}, + List: []ContactCard{a, b}, } } @@ -1899,7 +1883,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { Description: "James Holden will be confirmed as the President of the Transport Union, in room 2201 on station TSL-5.", DescriptionContentType: "text/plain", Links: map[string]jscalendar.Link{ - "aig1oh": jscalendar.Link{ + "aig1oh": { Type: jscalendar.LinkType, Href: "https://expanse.fandom.com/wiki/TSL-5", ContentType: "text/html", @@ -1916,7 +1900,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { }, ShowWithoutTime: false, Locations: map[string]jscalendar.Location{ - "eigha6": jscalendar.Location{ + "eigha6": { Type: jscalendar.LocationType, Name: "Room 2201", LocationTypes: map[jscalendar.LocationTypeOption]bool{ @@ -1924,7 +1908,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { }, Coordinates: "geo:40.7495,-73.9681", Links: map[string]jscalendar.Link{ - "ohb6qu": jscalendar.Link{ + "ohb6qu": { Type: jscalendar.LinkType, Href: "https://nss.org/what-is-l5/", ContentType: "text/html", @@ -1937,7 +1921,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { Sequence: 0, MainLocationId: "eigha6", VirtualLocations: map[string]jscalendar.VirtualLocation{ - "eec4ei": jscalendar.VirtualLocation{ + "eec4ei": { Type: jscalendar.VirtualLocationType, Name: "OpenTalk", Uri: "https://earth.gov.example.com/opentalk/l5/2022", @@ -1953,7 +1937,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { Privacy: jscalendar.PrivacyPublic, SentBy: "avasarala@earth.gov.example.com", Participants: map[string]jscalendar.Participant{ - "xaku3f": jscalendar.Participant{ + "xaku3f": { Type: jscalendar.ParticipantType, Name: "Christjen Avasarala", Email: "crissy@earth.gov.example.com", @@ -1965,7 +1949,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { }, ParticipationStatus: jscalendar.ParticipationStatusAccepted, }, - "chao1a": jscalendar.Participant{ + "chao1a": { Type: jscalendar.ParticipantType, Name: "Camina Drummer", Email: "camina@opa.org.example.com", @@ -1978,7 +1962,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { ExpectReply: true, InvitedBy: "xaku3f", }, - "ees0oo": jscalendar.Participant{ + "ees0oo": { Type: jscalendar.ParticipantType, Name: "James Holden", Email: "james.holden@rocinante.space", @@ -1992,7 +1976,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { }, }, Alerts: map[string]jscalendar.Alert{ - "kus9fa": jscalendar.Alert{ + "kus9fa": { Type: jscalendar.AlertType, Action: jscalendar.AlertActionDisplay, Trigger: jscalendar.OffsetTrigger{ @@ -2001,7 +1985,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent { RelativeTo: jscalendar.RelativeToStart, }, }, - "lohve9": jscalendar.Alert{ + "lohve9": { Type: jscalendar.AlertType, Action: jscalendar.AlertActionDisplay, Trigger: jscalendar.OffsetTrigger{ @@ -2047,7 +2031,7 @@ func (e Exemplar) ContactCardChanges() (ContactCardChanges, string, string) { OldState: "xai3iiraipoo", NewState: "ni7thah7eeY4", HasMoreChanges: true, - Created: []jscontact.ContactCard{c}, + Created: []ContactCard{c}, Destroyed: []string{"eaae", "bcba"}, }, "A created ContactCard and two deleted ones", "created" } @@ -2058,7 +2042,7 @@ func (e Exemplar) OtherContactCardChanges() (ContactCardChanges, string, string) OldState: "xai3iiraipoo", NewState: "ni7thah7eeY4", HasMoreChanges: false, - Updated: []jscontact.ContactCard{c}, + Updated: []ContactCard{c}, }, "An updated ContactCard", "updated" } diff --git a/pkg/jmap/model_test.go b/pkg/jmap/model_test.go index e44b1f9234..2ee10e2229 100644 --- a/pkg/jmap/model_test.go +++ b/pkg/jmap/model_test.go @@ -1,9 +1,12 @@ package jmap import ( + "encoding/json" "strings" "testing" + "time" + "github.com/opencloud-eu/opencloud/pkg/jscontact" "github.com/stretchr/testify/require" ) @@ -17,3 +20,606 @@ func TestObjectNames(t *testing.T) { //NOSONAR require.Equal(prefix, v) } } + +func jsoneq[X any](t *testing.T, expected string, object X) { + data, err := json.MarshalIndent(object, "", "") + require.NoError(t, err) + require.JSONEq(t, expected, string(data)) + + var rec X + err = json.Unmarshal(data, &rec) + require.NoError(t, err) + require.Equal(t, object, rec) +} + +func TestContactCard(t *testing.T) { + created, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") + require.NoError(t, err) + + updated, err := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00") + require.NoError(t, err) + + jsoneq(t, `{ + "@type": "Card", + "kind": "group", + "id": "20fba820-2f8e-432d-94f1-5abbb59d3ed7", + "addressBookIds": { + "79047052-ae0e-4299-8860-5bff1a139f3d": true, + "44eb6105-08c1-458b-895e-4ad1149dfabd": true + }, + "version": "1.0", + "created": "2025-09-25T18:26:14.094725532+02:00", + "language": "fr-BE", + "members": { + "314815dd-81c8-4640-aace-6dc83121616d": true, + "c528b277-d8cb-45f2-b7df-1aa3df817463": true, + "81dea240-c0a4-4929-82e7-79e713a8bbe4": true + }, + "prodId": "OpenCloud Groupware 1.0", + "relatedTo": { + "urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": { + "@type": "Relation", + "relation": { + "contact": true + } + }, + "urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": { + "relation": { + "emergency": true, + "spouse": true + } + } + }, + "uid": "1091f2bb-6ae6-4074-bb64-df74071d7033", + "updated": "2025-09-26T09:58:01+02:00", + "name": { + "@type": "Name", + "components": [ + {"@type": "NameComponent", "value": "OpenCloud", "kind": "surname"}, + {"value": " ", "kind": "separator"}, + {"value": "Team", "kind": "surname2"} + ], + "isOrdered": true, + "defaultSeparator": ", ", + "sortAs": { + "surname": "OpenCloud Team" + }, + "full": "OpenCloud Team" + }, + "nicknames": { + "a": { + "@type": "Nickname", + "name": "The Team", + "contexts": { + "work": true + }, + "pref": 1 + } + }, + "organizations": { + "o": { + "@type": "Organization", + "name": "OpenCloud GmbH", + "units": [ + {"@type": "OrgUnit", "name": "Marketing", "sortAs": "marketing"}, + {"@type": "OrgUnit", "name": "Sales"}, + {"name": "Operations", "sortAs": "ops"} + ], + "sortAs": "opencloud", + "contexts": { + "work": true + } + } + }, + "speakToAs": { + "@type": "SpeakToAs", + "grammaticalGender": "inanimate", + "pronouns": { + "p": { + "@type": "Pronouns", + "pronouns": "it", + "contexts": { + "work": true + }, + "pref": 1 + } + } + }, + "titles": { + "t": { + "@type": "Title", + "name": "The", + "kind": "title", + "organizationId": "o" + } + }, + "emails": { + "e": { + "@type": "EmailAddress", + "address": "info@opencloud.eu.example.com", + "contexts": { + "work": true + }, + "pref": 1, + "label": "work" + } + }, + "onlineServices": { + "s": { + "@type": "OnlineService", + "service": "The Misinformation Game", + "uri": "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251", + "user": "opencloudeu", + "contexts": { + "work": true + }, + "pref": 1, + "label": "imaginary" + } + }, + "phones": { + "p": { + "@type": "Phone", + "number": "+1-804-222-1111", + "features": { + "voice": true, + "text": true + }, + "contexts": { + "work": true + }, + "pref": 1, + "label": "imaginary" + } + }, + "preferredLanguages": { + "wa": { + "@type": "LanguagePref", + "language": "wa-BE", + "contexts": { + "private": true + }, + "pref": 1 + }, + "de": { + "language": "de-DE", + "contexts": { + "work": true + }, + "pref": 2 + } + }, + "calendars": { + "c": { + "@type": "Calendar", + "kind": "calendar", + "uri": "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2", + "mediaType": "application/jscontact+json", + "contexts": { + "work": true + }, + "pref": 1, + "label": "work" + } + }, + "schedulingAddresses": { + "s": { + "@type": "SchedulingAddress", + "uri": "mailto:scheduling@opencloud.eu.example.com", + "contexts": { + "work": true + }, + "pref": 1, + "label": "work" + } + }, + "addresses": { + "k26": { + "@type": "Address", + "components": [ + {"@type": "AddressComponent", "kind": "block", "value": "2-7"}, + {"kind": "separator", "value": "-"}, + {"kind": "number", "value": "2"}, + {"kind": "separator", "value": " "}, + {"kind": "district", "value": "Marunouchi"}, + {"kind": "locality", "value": "Chiyoda-ku"}, + {"kind": "region", "value": "Tokyo"}, + {"kind": "separator", "value": " "}, + {"kind": "postcode", "value": "100-8994"} + ], + "isOrdered": true, + "defaultSeparator": ", ", + "full": "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994", + "countryCode": "JP", + "coordinates": "geo:35.6796373,139.7616907", + "timeZone": "JST", + "contexts": { + "delivery": true, + "work": true + }, + "pref": 2 + } + }, + "cryptoKeys": { + "k1": { + "@type": "CryptoKey", + "uri": "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36", + "mediaType": "application/pgp-keys", + "contexts": { + "work": true + }, + "pref": 1, + "label": "keys" + } + }, + "directories": { + "d1": { + "@type": "Directory", + "kind": "entry", + "uri": "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722", + "listAs": 1 + } + }, + "links": { + "r1": { + "@type": "Link", + "kind": "contact", + "uri": "mailto:contact@opencloud.eu.example.com", + "contexts": { + "work": true + } + } + }, + "media": { + "m": { + "@type": "Media", + "kind": "logo", + "uri": "https://opencloud.eu.example.com/opencloud.svg", + "mediaType": "image/svg+xml", + "contexts": { + "work": true + }, + "pref": 123, + "label": "svg", + "blobId": "53feefbabeb146fcbe3e59e91462fa5f" + } + }, + "anniversaries": { + "birth": { + "@type": "Anniversary", + "kind": "birth", + "date": { + "@type": "PartialDate", + "year": 2025, + "month": 9, + "day": 26, + "calendarScale": "iso8601" + } + } + }, + "keywords": { + "imaginary": true, + "test": true + }, + "notes": { + "n1": { + "@type": "Note", + "note": "This is a note.", + "created": "2025-09-25T18:26:14.094725532+02:00", + "author": { + "@type": "Author", + "name": "Test Data", + "uri": "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3" + } + } + }, + "personalInfo": { + "p1": { + "@type": "PersonalInfo", + "kind": "expertise", + "value": "Clouds", + "level": "high", + "listAs": 1, + "label": "experts" + } + }, + "localizations": { + "fr": { + "personalInfo": { + "value": "Nuages" + } + } + } + }`, ContactCard{ + Type: jscontact.ContactCardType, + Kind: jscontact.ContactCardKindGroup, + Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7", + AddressBookIds: map[string]bool{ + "79047052-ae0e-4299-8860-5bff1a139f3d": true, + "44eb6105-08c1-458b-895e-4ad1149dfabd": true, + }, + Version: jscontact.JSContactVersion_1_0, + Created: created, + Language: "fr-BE", + Members: map[string]bool{ + "314815dd-81c8-4640-aace-6dc83121616d": true, + "c528b277-d8cb-45f2-b7df-1aa3df817463": true, + "81dea240-c0a4-4929-82e7-79e713a8bbe4": true, + }, + ProdId: "OpenCloud Groupware 1.0", + RelatedTo: map[string]jscontact.Relation{ + "urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": { + Type: jscontact.RelationType, + Relation: map[jscontact.Relationship]bool{ + jscontact.RelationContact: true, + }, + }, + "urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": { + Relation: map[jscontact.Relationship]bool{ + jscontact.RelationEmergency: true, + jscontact.RelationSpouse: true, + }, + }, + }, + Uid: "1091f2bb-6ae6-4074-bb64-df74071d7033", + Updated: updated, + Name: &jscontact.Name{ + Type: jscontact.NameType, + Components: []jscontact.NameComponent{ + {Type: jscontact.NameComponentType, Value: "OpenCloud", Kind: jscontact.NameComponentKindSurname}, + {Value: " ", Kind: jscontact.NameComponentKindSeparator}, + {Value: "Team", Kind: jscontact.NameComponentKindSurname2}, + }, + IsOrdered: true, + DefaultSeparator: ", ", + SortAs: map[string]string{ + string(jscontact.NameComponentKindSurname): "OpenCloud Team", + }, + Full: "OpenCloud Team", + }, + Nicknames: map[string]jscontact.Nickname{ + "a": { + Type: jscontact.NicknameType, + Name: "The Team", + Contexts: map[jscontact.NicknameContext]bool{ + jscontact.NicknameContextWork: true, + }, + Pref: 1, + }, + }, + Organizations: map[string]jscontact.Organization{ + "o": { + Type: jscontact.OrganizationType, + Name: "OpenCloud GmbH", + Units: []jscontact.OrgUnit{ + {Type: jscontact.OrgUnitType, Name: "Marketing", SortAs: "marketing"}, + {Type: jscontact.OrgUnitType, Name: "Sales"}, + {Name: "Operations", SortAs: "ops"}, + }, + SortAs: "opencloud", + Contexts: map[jscontact.OrganizationContext]bool{ + jscontact.OrganizationContextWork: true, + }, + }, + }, + SpeakToAs: &jscontact.SpeakToAs{ + Type: jscontact.SpeakToAsType, + GrammaticalGender: jscontact.GrammaticalGenderInanimate, + Pronouns: map[string]jscontact.Pronouns{ + "p": { + Type: jscontact.PronounsType, + Pronouns: "it", + Contexts: map[jscontact.PronounsContext]bool{ + jscontact.PronounsContextWork: true, + }, + Pref: 1, + }, + }, + }, + Titles: map[string]jscontact.Title{ + "t": { + Type: jscontact.TitleType, + Name: "The", + Kind: jscontact.TitleKindTitle, + OrganizationId: "o", + }, + }, + Emails: map[string]jscontact.EmailAddress{ + "e": { + Type: jscontact.EmailAddressType, + Address: "info@opencloud.eu.example.com", + Contexts: map[jscontact.EmailAddressContext]bool{ + jscontact.EmailAddressContextWork: true, + }, + Pref: 1, + Label: "work", + }, + }, + OnlineServices: map[string]jscontact.OnlineService{ + "s": { + Type: jscontact.OnlineServiceType, + Service: "The Misinformation Game", + Uri: "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251", + User: "opencloudeu", + Contexts: map[jscontact.OnlineServiceContext]bool{ + jscontact.OnlineServiceContextWork: true, + }, + Pref: 1, + Label: "imaginary", + }, + }, + Phones: map[string]jscontact.Phone{ + "p": { + Type: jscontact.PhoneType, + Number: "+1-804-222-1111", + Features: map[jscontact.PhoneFeature]bool{ + jscontact.PhoneFeatureVoice: true, + jscontact.PhoneFeatureText: true, + }, + Contexts: map[jscontact.PhoneContext]bool{ + jscontact.PhoneContextWork: true, + }, + Pref: 1, + Label: "imaginary", + }, + }, + PreferredLanguages: map[string]jscontact.LanguagePref{ + "wa": { + Type: jscontact.LanguagePrefType, + Language: "wa-BE", + Contexts: map[jscontact.LanguagePrefContext]bool{ + jscontact.LanguagePrefContextPrivate: true, + }, + Pref: 1, + }, + "de": { + Language: "de-DE", + Contexts: map[jscontact.LanguagePrefContext]bool{ + jscontact.LanguagePrefContextWork: true, + }, + Pref: 2, + }, + }, + Calendars: map[string]jscontact.Calendar{ + "c": { + Type: jscontact.CalendarType, + Kind: jscontact.CalendarKindCalendar, + Uri: "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2", + MediaType: "application/jscontact+json", + Contexts: map[jscontact.CalendarContext]bool{ + jscontact.CalendarContextWork: true, + }, + Pref: 1, + Label: "work", + }, + }, + SchedulingAddresses: map[string]jscontact.SchedulingAddress{ + "s": { + Type: jscontact.SchedulingAddressType, + Uri: "mailto:scheduling@opencloud.eu.example.com", + Contexts: map[jscontact.SchedulingAddressContext]bool{ + jscontact.SchedulingAddressContextWork: true, + }, + Pref: 1, + Label: "work", + }, + }, + Addresses: map[string]jscontact.Address{ + "k26": { + Type: jscontact.AddressType, + Components: []jscontact.AddressComponent{ + {Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindBlock, Value: "2-7"}, + {Kind: jscontact.AddressComponentKindSeparator, Value: "-"}, + {Kind: jscontact.AddressComponentKindNumber, Value: "2"}, + {Kind: jscontact.AddressComponentKindSeparator, Value: " "}, + {Kind: jscontact.AddressComponentKindDistrict, Value: "Marunouchi"}, + {Kind: jscontact.AddressComponentKindLocality, Value: "Chiyoda-ku"}, + {Kind: jscontact.AddressComponentKindRegion, Value: "Tokyo"}, + {Kind: jscontact.AddressComponentKindSeparator, Value: " "}, + {Kind: jscontact.AddressComponentKindPostcode, Value: "100-8994"}, + }, + IsOrdered: true, + DefaultSeparator: ", ", + Full: "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994", + CountryCode: "JP", + Coordinates: "geo:35.6796373,139.7616907", + TimeZone: "JST", + Contexts: map[jscontact.AddressContext]bool{ + jscontact.AddressContextDelivery: true, + jscontact.AddressContextWork: true, + }, + Pref: 2, + }, + }, + CryptoKeys: map[string]jscontact.CryptoKey{ + "k1": { + Type: jscontact.CryptoKeyType, + Uri: "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36", + MediaType: "application/pgp-keys", + Contexts: map[jscontact.CryptoKeyContext]bool{ + jscontact.CryptoKeyContextWork: true, + }, + Pref: 1, + Label: "keys", + }, + }, + Directories: map[string]jscontact.Directory{ + "d1": { + Type: jscontact.DirectoryType, + Kind: jscontact.DirectoryKindEntry, + Uri: "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722", + ListAs: 1, + }, + }, + Links: map[string]jscontact.Link{ + "r1": { + Type: jscontact.LinkType, + Kind: jscontact.LinkKindContact, + Contexts: map[jscontact.LinkContext]bool{ + jscontact.LinkContextWork: true, + }, + Uri: "mailto:contact@opencloud.eu.example.com", + }, + }, + Media: map[string]jscontact.Media{ + "m": { + Type: jscontact.MediaType, + Kind: jscontact.MediaKindLogo, + Uri: "https://opencloud.eu.example.com/opencloud.svg", + MediaType: "image/svg+xml", + Contexts: map[jscontact.MediaContext]bool{ + jscontact.MediaContextWork: true, + }, + Pref: 123, + Label: "svg", + BlobId: "53feefbabeb146fcbe3e59e91462fa5f", + }, + }, + Anniversaries: map[string]jscontact.Anniversary{ + "birth": { + Type: jscontact.AnniversaryType, + Kind: jscontact.AnniversaryKindBirth, + Date: &jscontact.PartialDate{ + Type: jscontact.PartialDateType, + Year: 2025, + Month: 9, + Day: 26, + CalendarScale: "iso8601", + }, + }, + }, + Keywords: map[string]bool{ + "imaginary": true, + "test": true, + }, + Notes: map[string]jscontact.Note{ + "n1": { + Type: jscontact.NoteType, + Note: "This is a note.", + Created: created, + Author: &jscontact.Author{ + Type: jscontact.AuthorType, + Name: "Test Data", + Uri: "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3", + }, + }, + }, + PersonalInfo: map[string]jscontact.PersonalInfo{ + "p1": { + Type: jscontact.PersonalInfoType, + Kind: jscontact.PersonalInfoKindExpertise, + Value: "Clouds", + Level: jscontact.PersonalInfoLevelHigh, + ListAs: 1, + Label: "experts", + }, + }, + Localizations: map[string]jscontact.PatchObject{ + "fr": { + "personalInfo": map[string]any{ + "value": "Nuages", + }, + }, + }, + }) +} diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 117fee7ac9..94cacfe42b 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -9,7 +9,68 @@ import ( "github.com/rs/zerolog" ) -func get[GETREQ GetCommand, GETRESP GetResponse, RESP any]( //NOSONAR +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, + session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (GETRESP, SessionState, State, Language, Error) { + var getresp GETRESP + return get(client, name, f.Namespaces(), + f.CreateGetCommand, + getresp, + identity1, + accountId, session, ctx, logger, acceptLanguage, ids, + ) +} + +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, + session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]T, SessionState, State, Language, Error) { + var getresp GETRESP + return getA(client, name, f.Namespaces(), + f.CreateGetCommand, + getresp, + accountId, session, ctx, logger, acceptLanguage, ids, + ) +} + +func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR client *Client, name string, using []JmapNamespace, getCommandFactory func(string, []string) GETREQ, _ GETRESP, @@ -38,7 +99,42 @@ func get[GETREQ GetCommand, GETRESP GetResponse, RESP any]( //NOSONAR }) } -func getN[GETREQ GetCommand, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR +func getA[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T]]( //NOSONAR + client *Client, name string, using []JmapNamespace, + getCommandFactory func(string, []string) GETREQ, + resp GETRESP, + accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) ([]T, SessionState, State, Language, Error) { + return get(client, name, using, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, session, ctx, logger, acceptLanguage, ids) +} + +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, + session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) { + var getresp GETRESP + return getAN(client, name, f.Namespaces(), + f.CreateGetCommand, + getresp, + respMapper, + accountIds, session, ctx, logger, acceptLanguage, ids, + ) +} + +func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR + client *Client, name string, using []JmapNamespace, + getCommandFactory func(string, []string) GETREQ, + resp GETRESP, + respMapper func(map[string][]T) RESP, + accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) { + return getN(client, name, using, getCommandFactory, resp, + func(r GETRESP) []T { return r.GetList() }, + respMapper, + accountIds, session, ctx, logger, acceptLanguage, ids, + ) +} + +func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR client *Client, name string, using []JmapNamespace, getCommandFactory func(string, []string) GETREQ, _ GETRESP, @@ -80,7 +176,7 @@ func getN[GETREQ GetCommand, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR }) } -func create[T any, C any, SETREQ SetCommand, GETREQ GetCommand, SETRESP SetResponse, GETRESP GetResponse]( //NOSONAR +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, setCommandFactory func(string, map[string]C) SETREQ, getCommandFactory func(string, string) GETREQ, @@ -139,7 +235,7 @@ func create[T any, C any, SETREQ SetCommand, GETREQ GetCommand, SETRESP SetRespo }) } -func destroy[REQ SetCommand, RESP SetResponse](client *Client, name string, using []JmapNamespace, //NOSONAR +func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR setCommandFactory func(string, []string) REQ, _ RESP, accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { logger = client.logger(name, session, logger) @@ -162,7 +258,23 @@ func destroy[REQ SetCommand, RESP SetResponse](client *Client, name string, usin }) } -func changes[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR +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, + changesCommandFactory func() CHANGESREQ, + changesResp CHANGESRESP, + _ GETRESP, + getCommandFactory func(string, string) GETREQ, + respMapper func(State, State, bool, []T, []T, []string) RESP, + session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) { + + return changes(client, name, using, changesCommandFactory, changesResp, getCommandFactory, + func(r GETRESP) []T { return r.GetList() }, + respMapper, + session, ctx, logger, acceptLanguage, + ) +} + +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, changesCommandFactory func() CHANGESREQ, _ CHANGESRESP, @@ -216,7 +328,7 @@ func changes[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesRe }) } -func changesN[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, CHANGESITEM any, RESP any]( //NOSONAR +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, accountIds []string, sinceStateMap map[string]State, changesCommandFactory func(string, State) CHANGESREQ, @@ -301,7 +413,7 @@ func changesN[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesR }) } -func updates[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR +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, changesCommandFactory func() CHANGESREQ, _ CHANGESRESP, @@ -343,7 +455,7 @@ func updates[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesRe }) } -func update[CHANGES Change, SET SetCommand, GET GetCommand, RESP any, SETRESP SetResponse, GETRESP GetResponse](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]](client *Client, name string, using []JmapNamespace, //NOSONAR setCommandFactory func(map[string]PatchObject) SET, getCommandFactory func(string) GET, notUpdatedExtractor func(SETRESP) map[string]SetError, diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index 678ff9ab96..08790a8c3e 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -14,7 +14,6 @@ import ( "github.com/mitchellh/mapstructure" "github.com/opencloud-eu/opencloud/pkg/jscalendar" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/pkg/structs" ) type eventListeners[T any] struct { @@ -87,6 +86,7 @@ func command[T any](api ApiClient, //NOSONAR for _, mr := range response.MethodResponses { if mr.Command == ErrorCommand { if errorParameters, ok := mr.Parameters.(ErrorResponse); ok { + // TODO deal with stateMismatch differently, as it's not an error per se, but rather "optimistic update" code := JmapErrorServerFail switch errorParameters.Type { case MethodLevelErrorServerUnavailable: @@ -233,19 +233,15 @@ func tryRetrieveResponseMatchParameters[T any](logger *log.Logger, data *Respons return true, nil } -func retrieveGet[C GetCommand, T GetResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error { +func retrieveGet[T Foo, C GetCommand[T], R GetResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error { return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target) } -func retrieveSet[C SetCommand, T SetResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error { +func retrieveSet[T Foo, C SetCommand[T], R SetResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error { return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target) } -func retrieveQuery[C QueryCommand, T QueryResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error { - return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target) -} - -func retrieveChanges[C ChangesCommand, T ChangesResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error { +func retrieveChanges[T Foo, C ChangesCommand[T], R ChangesResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error { return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target) } @@ -304,9 +300,11 @@ func squashState(all map[string]State) State { return squashStateFunc(all, func(s State) State { return s }) } +/* func squashStates(states ...State) State { return State(strings.Join(structs.Map(states, func(s State) string { return string(s) }), ",")) } +*/ func squashKeyedStates(m map[string]State) State { return squashStateFunc(m, identity1) @@ -395,6 +393,9 @@ func identity1[T any](t T) T { return t } +func list[T Foo, GETRESP GetResponse[T]](r GETRESP) []T { return r.GetList() } +func getid[T Idable](r T) string { return r.GetId() } + func posUIntPtr(i uint) *uint { if i > 0 { return &i diff --git a/pkg/jscontact/model.go b/pkg/jscontact/model.go index 17cf4d5492..3b7b5e0976 100644 --- a/pkg/jscontact/model.go +++ b/pkg/jscontact/model.go @@ -2260,6 +2260,8 @@ type ContactCard struct { PersonalInfo map[string]PersonalInfo `json:"personalInfo,omitempty"` } +func (f ContactCard) GetId() string { return f.Id } + const ( ContactCardPropertyId = "id" ContactCardPropertyAddressBookIds = "addressBookIds" diff --git a/pkg/jscontact/model_test.go b/pkg/jscontact/model_test.go index f70eeabb7c..8bc061f80d 100644 --- a/pkg/jscontact/model_test.go +++ b/pkg/jscontact/model_test.go @@ -658,595 +658,3 @@ func TestPersonalInfo(t *testing.T) { Label: "opa", }) } - -func TestContactCard(t *testing.T) { - created, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") - require.NoError(t, err) - - updated, err := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00") - require.NoError(t, err) - - jsoneq(t, `{ - "@type": "Card", - "kind": "group", - "id": "20fba820-2f8e-432d-94f1-5abbb59d3ed7", - "addressBookIds": { - "79047052-ae0e-4299-8860-5bff1a139f3d": true, - "44eb6105-08c1-458b-895e-4ad1149dfabd": true - }, - "version": "1.0", - "created": "2025-09-25T18:26:14.094725532+02:00", - "language": "fr-BE", - "members": { - "314815dd-81c8-4640-aace-6dc83121616d": true, - "c528b277-d8cb-45f2-b7df-1aa3df817463": true, - "81dea240-c0a4-4929-82e7-79e713a8bbe4": true - }, - "prodId": "OpenCloud Groupware 1.0", - "relatedTo": { - "urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": { - "@type": "Relation", - "relation": { - "contact": true - } - }, - "urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": { - "relation": { - "emergency": true, - "spouse": true - } - } - }, - "uid": "1091f2bb-6ae6-4074-bb64-df74071d7033", - "updated": "2025-09-26T09:58:01+02:00", - "name": { - "@type": "Name", - "components": [ - {"@type": "NameComponent", "value": "OpenCloud", "kind": "surname"}, - {"value": " ", "kind": "separator"}, - {"value": "Team", "kind": "surname2"} - ], - "isOrdered": true, - "defaultSeparator": ", ", - "sortAs": { - "surname": "OpenCloud Team" - }, - "full": "OpenCloud Team" - }, - "nicknames": { - "a": { - "@type": "Nickname", - "name": "The Team", - "contexts": { - "work": true - }, - "pref": 1 - } - }, - "organizations": { - "o": { - "@type": "Organization", - "name": "OpenCloud GmbH", - "units": [ - {"@type": "OrgUnit", "name": "Marketing", "sortAs": "marketing"}, - {"@type": "OrgUnit", "name": "Sales"}, - {"name": "Operations", "sortAs": "ops"} - ], - "sortAs": "opencloud", - "contexts": { - "work": true - } - } - }, - "speakToAs": { - "@type": "SpeakToAs", - "grammaticalGender": "inanimate", - "pronouns": { - "p": { - "@type": "Pronouns", - "pronouns": "it", - "contexts": { - "work": true - }, - "pref": 1 - } - } - }, - "titles": { - "t": { - "@type": "Title", - "name": "The", - "kind": "title", - "organizationId": "o" - } - }, - "emails": { - "e": { - "@type": "EmailAddress", - "address": "info@opencloud.eu.example.com", - "contexts": { - "work": true - }, - "pref": 1, - "label": "work" - } - }, - "onlineServices": { - "s": { - "@type": "OnlineService", - "service": "The Misinformation Game", - "uri": "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251", - "user": "opencloudeu", - "contexts": { - "work": true - }, - "pref": 1, - "label": "imaginary" - } - }, - "phones": { - "p": { - "@type": "Phone", - "number": "+1-804-222-1111", - "features": { - "voice": true, - "text": true - }, - "contexts": { - "work": true - }, - "pref": 1, - "label": "imaginary" - } - }, - "preferredLanguages": { - "wa": { - "@type": "LanguagePref", - "language": "wa-BE", - "contexts": { - "private": true - }, - "pref": 1 - }, - "de": { - "language": "de-DE", - "contexts": { - "work": true - }, - "pref": 2 - } - }, - "calendars": { - "c": { - "@type": "Calendar", - "kind": "calendar", - "uri": "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2", - "mediaType": "application/jscontact+json", - "contexts": { - "work": true - }, - "pref": 1, - "label": "work" - } - }, - "schedulingAddresses": { - "s": { - "@type": "SchedulingAddress", - "uri": "mailto:scheduling@opencloud.eu.example.com", - "contexts": { - "work": true - }, - "pref": 1, - "label": "work" - } - }, - "addresses": { - "k26": { - "@type": "Address", - "components": [ - {"@type": "AddressComponent", "kind": "block", "value": "2-7"}, - {"kind": "separator", "value": "-"}, - {"kind": "number", "value": "2"}, - {"kind": "separator", "value": " "}, - {"kind": "district", "value": "Marunouchi"}, - {"kind": "locality", "value": "Chiyoda-ku"}, - {"kind": "region", "value": "Tokyo"}, - {"kind": "separator", "value": " "}, - {"kind": "postcode", "value": "100-8994"} - ], - "isOrdered": true, - "defaultSeparator": ", ", - "full": "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994", - "countryCode": "JP", - "coordinates": "geo:35.6796373,139.7616907", - "timeZone": "JST", - "contexts": { - "delivery": true, - "work": true - }, - "pref": 2 - } - }, - "cryptoKeys": { - "k1": { - "@type": "CryptoKey", - "uri": "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36", - "mediaType": "application/pgp-keys", - "contexts": { - "work": true - }, - "pref": 1, - "label": "keys" - } - }, - "directories": { - "d1": { - "@type": "Directory", - "kind": "entry", - "uri": "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722", - "listAs": 1 - } - }, - "links": { - "r1": { - "@type": "Link", - "kind": "contact", - "uri": "mailto:contact@opencloud.eu.example.com", - "contexts": { - "work": true - } - } - }, - "media": { - "m": { - "@type": "Media", - "kind": "logo", - "uri": "https://opencloud.eu.example.com/opencloud.svg", - "mediaType": "image/svg+xml", - "contexts": { - "work": true - }, - "pref": 123, - "label": "svg", - "blobId": "53feefbabeb146fcbe3e59e91462fa5f" - } - }, - "anniversaries": { - "birth": { - "@type": "Anniversary", - "kind": "birth", - "date": { - "@type": "PartialDate", - "year": 2025, - "month": 9, - "day": 26, - "calendarScale": "iso8601" - } - } - }, - "keywords": { - "imaginary": true, - "test": true - }, - "notes": { - "n1": { - "@type": "Note", - "note": "This is a note.", - "created": "2025-09-25T18:26:14.094725532+02:00", - "author": { - "@type": "Author", - "name": "Test Data", - "uri": "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3" - } - } - }, - "personalInfo": { - "p1": { - "@type": "PersonalInfo", - "kind": "expertise", - "value": "Clouds", - "level": "high", - "listAs": 1, - "label": "experts" - } - }, - "localizations": { - "fr": { - "personalInfo": { - "value": "Nuages" - } - } - } - }`, ContactCard{ - Type: ContactCardType, - Kind: ContactCardKindGroup, - Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7", - AddressBookIds: map[string]bool{ - "79047052-ae0e-4299-8860-5bff1a139f3d": true, - "44eb6105-08c1-458b-895e-4ad1149dfabd": true, - }, - Version: JSContactVersion_1_0, - Created: created, - Language: "fr-BE", - Members: map[string]bool{ - "314815dd-81c8-4640-aace-6dc83121616d": true, - "c528b277-d8cb-45f2-b7df-1aa3df817463": true, - "81dea240-c0a4-4929-82e7-79e713a8bbe4": true, - }, - ProdId: "OpenCloud Groupware 1.0", - RelatedTo: map[string]Relation{ - "urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": { - Type: RelationType, - Relation: map[Relationship]bool{ - RelationContact: true, - }, - }, - "urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": { - Relation: map[Relationship]bool{ - RelationEmergency: true, - RelationSpouse: true, - }, - }, - }, - Uid: "1091f2bb-6ae6-4074-bb64-df74071d7033", - Updated: updated, - Name: &Name{ - Type: NameType, - Components: []NameComponent{ - {Type: NameComponentType, Value: "OpenCloud", Kind: NameComponentKindSurname}, - {Value: " ", Kind: NameComponentKindSeparator}, - {Value: "Team", Kind: NameComponentKindSurname2}, - }, - IsOrdered: true, - DefaultSeparator: ", ", - SortAs: map[string]string{ - string(NameComponentKindSurname): "OpenCloud Team", - }, - Full: "OpenCloud Team", - }, - Nicknames: map[string]Nickname{ - "a": { - Type: NicknameType, - Name: "The Team", - Contexts: map[NicknameContext]bool{ - NicknameContextWork: true, - }, - Pref: 1, - }, - }, - Organizations: map[string]Organization{ - "o": { - Type: OrganizationType, - Name: "OpenCloud GmbH", - Units: []OrgUnit{ - {Type: OrgUnitType, Name: "Marketing", SortAs: "marketing"}, - {Type: OrgUnitType, Name: "Sales"}, - {Name: "Operations", SortAs: "ops"}, - }, - SortAs: "opencloud", - Contexts: map[OrganizationContext]bool{ - OrganizationContextWork: true, - }, - }, - }, - SpeakToAs: &SpeakToAs{ - Type: SpeakToAsType, - GrammaticalGender: GrammaticalGenderInanimate, - Pronouns: map[string]Pronouns{ - "p": { - Type: PronounsType, - Pronouns: "it", - Contexts: map[PronounsContext]bool{ - PronounsContextWork: true, - }, - Pref: 1, - }, - }, - }, - Titles: map[string]Title{ - "t": { - Type: TitleType, - Name: "The", - Kind: TitleKindTitle, - OrganizationId: "o", - }, - }, - Emails: map[string]EmailAddress{ - "e": { - Type: EmailAddressType, - Address: "info@opencloud.eu.example.com", - Contexts: map[EmailAddressContext]bool{ - EmailAddressContextWork: true, - }, - Pref: 1, - Label: "work", - }, - }, - OnlineServices: map[string]OnlineService{ - "s": { - Type: OnlineServiceType, - Service: "The Misinformation Game", - Uri: "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251", - User: "opencloudeu", - Contexts: map[OnlineServiceContext]bool{ - OnlineServiceContextWork: true, - }, - Pref: 1, - Label: "imaginary", - }, - }, - Phones: map[string]Phone{ - "p": { - Type: PhoneType, - Number: "+1-804-222-1111", - Features: map[PhoneFeature]bool{ - PhoneFeatureVoice: true, - PhoneFeatureText: true, - }, - Contexts: map[PhoneContext]bool{ - PhoneContextWork: true, - }, - Pref: 1, - Label: "imaginary", - }, - }, - PreferredLanguages: map[string]LanguagePref{ - "wa": { - Type: LanguagePrefType, - Language: "wa-BE", - Contexts: map[LanguagePrefContext]bool{ - LanguagePrefContextPrivate: true, - }, - Pref: 1, - }, - "de": { - Language: "de-DE", - Contexts: map[LanguagePrefContext]bool{ - LanguagePrefContextWork: true, - }, - Pref: 2, - }, - }, - Calendars: map[string]Calendar{ - "c": { - Type: CalendarType, - Kind: CalendarKindCalendar, - Uri: "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2", - MediaType: "application/jscontact+json", - Contexts: map[CalendarContext]bool{ - CalendarContextWork: true, - }, - Pref: 1, - Label: "work", - }, - }, - SchedulingAddresses: map[string]SchedulingAddress{ - "s": { - Type: SchedulingAddressType, - Uri: "mailto:scheduling@opencloud.eu.example.com", - Contexts: map[SchedulingAddressContext]bool{ - SchedulingAddressContextWork: true, - }, - Pref: 1, - Label: "work", - }, - }, - Addresses: map[string]Address{ - "k26": { - Type: AddressType, - Components: []AddressComponent{ - {Type: AddressComponentType, Kind: AddressComponentKindBlock, Value: "2-7"}, - {Kind: AddressComponentKindSeparator, Value: "-"}, - {Kind: AddressComponentKindNumber, Value: "2"}, - {Kind: AddressComponentKindSeparator, Value: " "}, - {Kind: AddressComponentKindDistrict, Value: "Marunouchi"}, - {Kind: AddressComponentKindLocality, Value: "Chiyoda-ku"}, - {Kind: AddressComponentKindRegion, Value: "Tokyo"}, - {Kind: AddressComponentKindSeparator, Value: " "}, - {Kind: AddressComponentKindPostcode, Value: "100-8994"}, - }, - IsOrdered: true, - DefaultSeparator: ", ", - Full: "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994", - CountryCode: "JP", - Coordinates: "geo:35.6796373,139.7616907", - TimeZone: "JST", - Contexts: map[AddressContext]bool{ - AddressContextDelivery: true, - AddressContextWork: true, - }, - Pref: 2, - }, - }, - CryptoKeys: map[string]CryptoKey{ - "k1": { - Type: CryptoKeyType, - Uri: "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36", - MediaType: "application/pgp-keys", - Contexts: map[CryptoKeyContext]bool{ - CryptoKeyContextWork: true, - }, - Pref: 1, - Label: "keys", - }, - }, - Directories: map[string]Directory{ - "d1": { - Type: DirectoryType, - Kind: DirectoryKindEntry, - Uri: "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722", - ListAs: 1, - }, - }, - Links: map[string]Link{ - "r1": { - Type: LinkType, - Kind: LinkKindContact, - Contexts: map[LinkContext]bool{ - LinkContextWork: true, - }, - Uri: "mailto:contact@opencloud.eu.example.com", - }, - }, - Media: map[string]Media{ - "m": { - Type: MediaType, - Kind: MediaKindLogo, - Uri: "https://opencloud.eu.example.com/opencloud.svg", - MediaType: "image/svg+xml", - Contexts: map[MediaContext]bool{ - MediaContextWork: true, - }, - Pref: 123, - Label: "svg", - BlobId: "53feefbabeb146fcbe3e59e91462fa5f", - }, - }, - Anniversaries: map[string]Anniversary{ - "birth": { - Type: AnniversaryType, - Kind: AnniversaryKindBirth, - Date: &PartialDate{ - Type: PartialDateType, - Year: 2025, - Month: 9, - Day: 26, - CalendarScale: "iso8601", - }, - }, - }, - Keywords: map[string]bool{ - "imaginary": true, - "test": true, - }, - Notes: map[string]Note{ - "n1": { - Type: NoteType, - Note: "This is a note.", - Created: created, - Author: &Author{ - Type: AuthorType, - Name: "Test Data", - Uri: "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3", - }, - }, - }, - PersonalInfo: map[string]PersonalInfo{ - "p1": { - Type: PersonalInfoType, - Kind: PersonalInfoKindExpertise, - Value: "Clouds", - Level: PersonalInfoLevelHigh, - ListAs: 1, - Label: "experts", - }, - }, - Localizations: map[string]PatchObject{ - "fr": { - "personalInfo": map[string]any{ - "value": "Nuages", - }, - }, - }, - }) -} diff --git a/services/groupware/pkg/groupware/api_addressbooks.go b/services/groupware/pkg/groupware/api_addressbooks.go index 462d2ad45f..d98b20fa8d 100644 --- a/services/groupware/pkg/groupware/api_addressbooks.go +++ b/services/groupware/pkg/groupware/api_addressbooks.go @@ -20,7 +20,7 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) { return req.jmapError(accountId, jerr, sessionState, lang) } - var body jmap.AddressBooksResponse = addressbooks + var body jmap.AddressBookGetResponse = addressbooks return req.respond(accountId, body, sessionState, AddressBookResponseObjectType, state) }) } @@ -47,10 +47,14 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) { return req.jmapError(accountId, jerr, sessionState, lang) } - if len(addressbooks.NotFound) > 0 { - return req.notFound(accountId, sessionState, AddressBookResponseObjectType, state) - } else { - return req.respond(accountId, addressbooks.AddressBooks[0], sessionState, AddressBookResponseObjectType, state) + switch len(addressbooks.List) { + case 0: + return req.notFound(accountId, sessionState, ContactResponseObjectType, state) + case 1: + return req.respond(accountId, addressbooks.List[0], sessionState, ContactResponseObjectType, state) + default: + logger.Error().Msgf("found %d addressbooks matching '%s' instead of 1", len(addressbooks.List), addressBookId) + return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState) } }) } diff --git a/services/groupware/pkg/groupware/api_calendars.go b/services/groupware/pkg/groupware/api_calendars.go index e9cd7a991f..251dd46f24 100644 --- a/services/groupware/pkg/groupware/api_calendars.go +++ b/services/groupware/pkg/groupware/api_calendars.go @@ -46,10 +46,14 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { return req.jmapError(accountId, jerr, sessionState, lang) } - if len(calendars.NotFound) > 0 { - return req.notFound(accountId, sessionState, CalendarResponseObjectType, state) - } else { - return req.respond(accountId, calendars.Calendars[0], sessionState, CalendarResponseObjectType, state) + switch len(calendars.List) { + case 0: + return req.notFound(accountId, sessionState, ContactResponseObjectType, state) + case 1: + return req.respond(accountId, calendars.List[0], sessionState, ContactResponseObjectType, state) + default: + logger.Error().Msgf("found %d calendars matching '%s' instead of 1", len(calendars.List), calendarId) + return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState) } }) } diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go index 47f1d78f5a..8bbf61a4ad 100644 --- a/services/groupware/pkg/groupware/api_contacts.go +++ b/services/groupware/pkg/groupware/api_contacts.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/opencloud-eu/opencloud/pkg/jmap" - "github.com/opencloud-eu/opencloud/pkg/jscontact" "github.com/opencloud-eu/opencloud/pkg/log" ) @@ -27,17 +26,17 @@ var ( */ // So we have to settle for this, as only 'updated' and 'created' are supported for now: DefaultContactSort = []jmap.ContactCardComparator{ - {Property: jscontact.ContactCardPropertyUpdated, IsAscending: true}, + {Property: jmap.ContactCardPropertyUpdated, IsAscending: true}, } SupportedContactSortingProperties = []string{ - jscontact.ContactCardPropertyUpdated, - jscontact.ContactCardPropertyCreated, + jmap.ContactCardPropertyUpdated, + jmap.ContactCardPropertyCreated, } ContactSortingPropertyMapping = map[string]string{ - "surname": string(jscontact.ContactCardPropertyName) + "/surname", - "given": string(jscontact.ContactCardPropertyName) + "/given", + "surname": string(jmap.ContactCardPropertyName) + "/surname", + "given": string(jmap.ContactCardPropertyName) + "/given", } ) @@ -114,15 +113,19 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) { l = l.Str(UriParamContactId, log.SafeString(contactId)) logger := log.From(l) - contactsById, sessionState, state, lang, jerr := g.jmap.GetContactCardsById(accountId, req.session, req.ctx, logger, req.language(), []string{contactId}) + contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, req.session, req.ctx, logger, req.language(), []string{contactId}) if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } - if contact, ok := contactsById[contactId]; ok { - return req.respond(accountId, contact, sessionState, ContactResponseObjectType, state) - } else { + switch len(contacts.List) { + case 0: return req.notFound(accountId, sessionState, ContactResponseObjectType, state) + case 1: + return req.respond(accountId, contacts.List[0], sessionState, ContactResponseObjectType, state) + default: + logger.Error().Msgf("found %d contacts matching '%s' instead of 1", len(contacts.List), contactId) + return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState) } }) } @@ -141,7 +144,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) { if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } - var body []jscontact.ContactCard = contacts + var body []jmap.ContactCard = contacts.List return req.respond(accountId, body, sessionState, ContactResponseObjectType, state) }) @@ -195,7 +198,7 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) { } l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId)) - var create jscontact.ContactCard + var create jmap.ContactCard err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set") if err != nil { return req.error(accountId, err) diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go index 1dac5ba515..ed30ce3e84 100644 --- a/services/groupware/pkg/groupware/api_mailbox.go +++ b/services/groupware/pkg/groupware/api_mailbox.go @@ -35,8 +35,8 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) { return req.jmapError(accountId, jerr, sessionState, lang) } - if len(mailboxes.Mailboxes) == 1 { - return req.respond(accountId, mailboxes.Mailboxes[0], sessionState, MailboxResponseObjectType, state) + if len(mailboxes.List) == 1 { + return req.respond(accountId, mailboxes.List[0], sessionState, MailboxResponseObjectType, state) } else { return req.notFound(accountId, sessionState, MailboxResponseObjectType, state) } diff --git a/services/groupware/pkg/groupware/api_quota.go b/services/groupware/pkg/groupware/api_quota.go index 1562acf8bf..f1ac84c169 100644 --- a/services/groupware/pkg/groupware/api_quota.go +++ b/services/groupware/pkg/groupware/api_quota.go @@ -24,9 +24,8 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) { if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } - for _, v := range res { - body := v.List - return req.respond(accountId, body, sessionState, QuotaResponseObjectType, state) + for _, quotas := range res { + return req.respond(accountId, quotas, sessionState, QuotaResponseObjectType, state) } return req.notFound(accountId, sessionState, QuotaResponseObjectType, state) }) diff --git a/services/groupware/pkg/groupware/error.go b/services/groupware/pkg/groupware/error.go index 3aeec680df..199a0d6631 100644 --- a/services/groupware/pkg/groupware/error.go +++ b/services/groupware/pkg/groupware/error.go @@ -208,6 +208,7 @@ const ( ErrorCodeInvalidSortSpecification = "INVSSP" ErrorCodeInvalidSortProperty = "INVSPR" ErrorCodeInvalidObjectState = "INVOST" + ErrorCodeMultipleIdMatches = "MIDMAT" ) var ( @@ -493,6 +494,12 @@ var ( Title: "Invalid Object State", Detail: "The request included an object state that does not exist.", } + ErrorMultipleIdMatches = GroupwareError{ + Status: http.StatusConflict, + Code: ErrorCodeMultipleIdMatches, + Title: "Multiple unique identifier matches", + Detail: "A supposedly unique identifier matched multiple objects.", + } ) type ErrorOpt interface {