From 3449b5465bb146fe92161671adff77dc33bf3b9b Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 9 Apr 2026 10:38:35 +0200
Subject: [PATCH] groupware: model refactoring, introducing typed interfaces
and Foo
* move ContactCard from jscontact to jmap, as it is actually a JMAP
specification item, but also was causing too many issues with
circular references from jscontact -> jmap
* introduce Foo, Idable, GetRequest, GetResponse, etc... types and
generics
* first attempt at a Foo factory type for Mailboxes, needs to be
expanded to further minimize repetition
* add more specialized template functions to avoid repetition
* introduce ChangesTemplate[T] for *Changes structs
---
go.mod | 2 +-
pkg/jmap/api_addressbook.go | 24 +-
pkg/jmap/api_calendar.go | 45 +-
pkg/jmap/api_changes.go | 3 -
pkg/jmap/api_contact.go | 85 +-
pkg/jmap/api_email.go | 9 +-
pkg/jmap/api_identity.go | 15 +-
pkg/jmap/api_mailbox.go | 74 +-
pkg/jmap/api_quota.go | 13 +-
pkg/jmap/integration_contact_test.go | 176 +--
pkg/jmap/integration_event_test.go | 180 ++-
pkg/jmap/integration_test.go | 124 +-
pkg/jmap/model.go | 1019 ++++++++++++-----
pkg/jmap/model_examples.go | 48 +-
pkg/jmap/model_test.go | 606 ++++++++++
pkg/jmap/templates.go | 128 ++-
pkg/jmap/tools.go | 17 +-
pkg/jscontact/model.go | 2 +
pkg/jscontact/model_test.go | 592 ----------
.../pkg/groupware/api_addressbooks.go | 14 +-
.../groupware/pkg/groupware/api_calendars.go | 12 +-
.../groupware/pkg/groupware/api_contacts.go | 27 +-
.../groupware/pkg/groupware/api_mailbox.go | 4 +-
services/groupware/pkg/groupware/api_quota.go | 5 +-
services/groupware/pkg/groupware/error.go | 7 +
25 files changed, 1994 insertions(+), 1237 deletions(-)
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 {