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
This commit is contained in:
Pascal Bleser
2026-04-09 10:38:35 +02:00
parent 49c2425172
commit 3449b5465b
25 changed files with 1994 additions and 1237 deletions

2
go.mod
View File

@@ -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

View File

@@ -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,

View File

@@ -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,
)
}

View File

@@ -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"))
}

View File

@@ -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
})
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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}),

View File

@@ -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")

View File

@@ -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)
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}

View File

@@ -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",
},
},
},
})
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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"

View File

@@ -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",
},
},
},
})
}

View File

@@ -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)
}
})
}

View File

@@ -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)
}
})
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
})

View File

@@ -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 {