groupware: framework refactorings + add support for /changes + add /objects

This commit is contained in:
Pascal Bleser
2026-03-26 16:25:45 +01:00
parent 96ee4e6db1
commit 2378eca6d8
31 changed files with 1568 additions and 501 deletions

View File

@@ -31,7 +31,7 @@ func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context
if len(response.List) != 1 {
logger.Error().Msgf("%T.List has %v entries instead of 1", response, len(response.List))
return nil, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
get := response.List[0]
return &get, response.State, nil
@@ -112,17 +112,17 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
if len(uploadResponse.Created) != 1 {
logger.Error().Msgf("%T.Created has %v entries instead of 1", uploadResponse, len(uploadResponse.Created))
return UploadedBlobWithHash{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
upload, ok := uploadResponse.Created["0"]
if !ok {
logger.Error().Msgf("%T.Created has no item '0'", uploadResponse)
return UploadedBlobWithHash{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
if len(getResponse.List) != 1 {
logger.Error().Msgf("%T.List has %v entries instead of 1", getResponse, len(getResponse.List))
return UploadedBlobWithHash{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedBlobWithHash{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
get := getResponse.List[0]

View File

@@ -45,6 +45,52 @@ func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Co
)
}
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"`
}
// Retrieve Calendar changes since a given state.
// @apidoc calendar,changes
func (j *Client) GetCalendarChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (CalendarChanges, SessionState, State, Language, Error) {
return changesTemplate(j, "GetCalendarChanges",
CommandCalendarChanges, CommandCalendarGet,
func() CalendarChangesCommand {
return CalendarChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
},
func(path string, rof string) CalendarGetRefCommand {
return CalendarGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp CalendarGetResponse) []Calendar { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []Calendar, destroyed []string) CalendarChanges {
return CalendarChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
func(resp CalendarGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
}
func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position uint, limit uint) (map[string][]CalendarEvent, SessionState, State, Language, Error) {
@@ -95,7 +141,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx
return nil, "", err
}
if len(response.NotFound) > 0 {
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
// TODO what to do when there are not-found calendarevents here? potentially nothing, they could have been deleted between query and get?
}
resp[accountId] = response.List
stateByAccountId[accountId] = response.State
@@ -104,6 +150,51 @@ 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"`
}
func (j *Client) GetCalendarEventChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger,
acceptLanguage string, sinceState State, maxChanges uint) (CalendarEventChanges, SessionState, State, Language, Error) {
return changesTemplate(j, "GetCalendarEventChanges",
CommandCalendarEventChanges, CommandCalendarEventGet,
func() CalendarEventChangesCommand {
return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
},
func(path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandCalendarEventChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp CalendarEventChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp CalendarEventGetResponse) []CalendarEvent { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []CalendarEvent, destroyed []string) CalendarEventChanges {
return CalendarEventChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
func(resp CalendarEventGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
}
func (j *Client) CreateCalendarEvent(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create CalendarEvent) (*CalendarEvent, SessionState, State, Language, Error) {
return createTemplate(j, "CreateCalendarEvent", CalendarEventType, CommandCalendarEventSet, CommandCalendarEventGet,
func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand {

144
pkg/jmap/api_changes.go Normal file
View File

@@ -0,0 +1,144 @@
package jmap
import (
"context"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/rs/zerolog"
)
type Changes struct {
MaxChanges uint `json:"maxchanges,omitzero"`
Mailboxes *MailboxChangesResponse `json:"mailboxes,omitempty"`
Emails *EmailChangesResponse `json:"emails,omitempty"`
Calendars *CalendarChangesResponse `json:"calendars,omitempty"`
Events *CalendarEventChangesResponse `json:"events,omitempty"`
Addressbooks *AddressBookChangesResponse `json:"addressbooks,omitempty"`
Contacts *ContactCardChangesResponse `json:"contacts,omitempty"`
}
type StateMap struct {
Mailboxes *State `json:"mailboxes,omitempty"`
Emails *State `json:"emails,omitempty"`
Calendars *State `json:"calendars,omitempty"`
Events *State `json:"events,omitempty"`
Addressbooks *State `json:"addressbooks,omitempty"`
Contacts *State `json:"contacts,omitempty"`
}
var _ zerolog.LogObjectMarshaler = StateMap{}
func (s StateMap) IsZero() bool {
return s.Mailboxes == nil && s.Emails == nil && s.Calendars == nil &&
s.Events == nil && s.Addressbooks == nil && s.Contacts == nil
}
func (s StateMap) MarshalZerologObject(e *zerolog.Event) {
if s.Mailboxes != nil {
e.Str("mailboxes", string(*s.Mailboxes))
}
if s.Emails != nil {
e.Str("emails", string(*s.Emails))
}
if s.Calendars != nil {
e.Str("calendars", string(*s.Calendars))
}
if s.Events != nil {
e.Str("events", string(*s.Events))
}
if s.Addressbooks != nil {
e.Str("addressbooks", string(*s.Addressbooks))
}
if s.Contacts != nil {
e.Str("contacts", string(*s.Contacts))
}
}
func (j *Client) GetChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, stateMap StateMap, maxChanges uint) (Changes, SessionState, State, Language, Error) { //NOSONAR
logger = log.From(j.logger("GetChanges", session, logger).With().Object("state", stateMap).Uint("maxChanges", maxChanges))
methodCalls := []Invocation{}
if stateMap.Mailboxes != nil {
methodCalls = append(methodCalls, invocation(CommandMailboxChanges, MailboxChangesCommand{AccountId: accountId, SinceState: *stateMap.Mailboxes, MaxChanges: posUIntPtr(maxChanges)}, "mailboxes"))
}
if stateMap.Emails != nil {
methodCalls = append(methodCalls, invocation(CommandEmailChanges, EmailChangesCommand{AccountId: accountId, SinceState: *stateMap.Emails, MaxChanges: posUIntPtr(maxChanges)}, "emails"))
}
if stateMap.Calendars != nil {
methodCalls = append(methodCalls, invocation(CommandCalendarChanges, CalendarChangesCommand{AccountId: accountId, SinceState: *stateMap.Calendars, MaxChanges: posUIntPtr(maxChanges)}, "calendars"))
}
if stateMap.Events != nil {
methodCalls = append(methodCalls, invocation(CommandCalendarEventChanges, CalendarEventChangesCommand{AccountId: accountId, SinceState: *stateMap.Events, MaxChanges: posUIntPtr(maxChanges)}, "events"))
}
if stateMap.Addressbooks != nil {
methodCalls = append(methodCalls, invocation(CommandAddressBookChanges, AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks"))
}
if stateMap.Addressbooks != nil {
methodCalls = append(methodCalls, invocation(CommandAddressBookChanges, AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks"))
}
if stateMap.Contacts != nil {
methodCalls = append(methodCalls, invocation(CommandContactCardChanges, ContactCardChangesCommand{AccountId: accountId, SinceState: *stateMap.Contacts, MaxChanges: posUIntPtr(maxChanges)}, "contacts"))
}
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
return Changes{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Changes, State, Error) {
changes := Changes{
MaxChanges: maxChanges,
}
states := map[string]State{}
var mailboxes MailboxChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandMailboxChanges, "mailboxes", &mailboxes); err != nil {
return Changes{}, "", err
} else if ok {
changes.Mailboxes = &mailboxes
states["mailbox"] = mailboxes.NewState
}
var emails EmailChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandEmailChanges, "emails", &emails); err != nil {
return Changes{}, "", err
} else if ok {
changes.Emails = &emails
states["emails"] = emails.NewState
}
var calendars CalendarChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandCalendarChanges, "calendars", &calendars); err != nil {
return Changes{}, "", err
} else if ok {
changes.Calendars = &calendars
states["calendars"] = calendars.NewState
}
var events CalendarEventChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandCalendarEventChanges, "events", &events); err != nil {
return Changes{}, "", err
} else if ok {
changes.Events = &events
states["events"] = events.NewState
}
var addressbooks AddressBookChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandAddressBookChanges, "addressbooks", &addressbooks); err != nil {
return Changes{}, "", err
} else if ok {
changes.Addressbooks = &addressbooks
states["addressbooks"] = addressbooks.NewState
}
var contacts ContactCardChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandContactCardChanges, "contacts", &contacts); err != nil {
return Changes{}, "", err
} else if ok {
changes.Contacts = &contacts
states["contacts"] = contacts.NewState
}
return changes, squashKeyedStates(states), nil
})
}

View File

@@ -37,6 +37,52 @@ func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context
})
}
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"`
}
// 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 changesTemplate(j, "GetAddressbookChanges",
CommandAddressBookChanges, CommandAddressBookGet,
func() AddressBookChangesCommand {
return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
},
func(path string, rof string) AddressBookGetRefCommand {
return AddressBookGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandAddressBookChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp AddressBookChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp AddressBookGetResponse) []AddressBook { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []AddressBook, destroyed []string) AddressBookChanges {
return AddressBookChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
func(resp AddressBookGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
}
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)
@@ -65,24 +111,14 @@ func (j *Client) GetContactCardsById(accountId string, session *Session, ctx con
func (j *Client) GetContactCards(accountId string, session *Session, ctx context.Context, logger *log.Logger,
acceptLanguage string, contactIds []string) ([]jscontact.ContactCard, SessionState, State, Language, Error) {
logger = j.logger("GetContactCards", session, logger)
cmd, err := j.request(session, logger, invocation(CommandContactCardGet, 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) ([]jscontact.ContactCard, State, Error) {
var response ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "0", &response)
if err != nil {
return nil, "", err
}
return response.List, response.State, nil
})
return getTemplate(j, "GetContactCards", CommandContactCardGet,
func(accountId string, ids []string) ContactCardGetCommand {
return ContactCardGetCommand{AccountId: accountId, Ids: contactIds}
},
func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List },
func(resp ContactCardGetResponse) State { return resp.State },
accountId, session, ctx, logger, acceptLanguage, contactIds,
)
}
type ContactCardChanges struct {
@@ -94,70 +130,40 @@ type ContactCardChanges struct {
Destroyed []string `json:"destroyed,omitempty"`
}
func (j *Client) GetContactCardsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger,
acceptLanguage string, sinceState string, maxChanges uint) (ContactCardChanges, SessionState, State, Language, Error) {
logger = j.logger("GetContactCards", session, logger)
maxChangesPtr := &maxChanges
if maxChanges < 1 {
maxChangesPtr = nil
}
cmd, err := j.request(session, logger,
invocation(CommandContactCardChanges, ContactCardChangesCommand{
AccountId: accountId,
SinceState: sinceState,
MaxChanges: maxChangesPtr,
}, "0"),
invocation(CommandContactCardGet, ContactCardGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
ResultOf: "0",
Name: CommandContactCardChanges,
Path: "/created",
},
}, "1"),
invocation(CommandContactCardGet, ContactCardGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
ResultOf: "0",
Name: CommandContactCardChanges,
Path: "/updated",
},
}, "2"),
func (j *Client) GetContactCardChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger,
acceptLanguage string, sinceState State, maxChanges uint) (ContactCardChanges, SessionState, State, Language, Error) {
return changesTemplate(j, "GetContactCardChanges",
CommandContactCardChanges, CommandContactCardGet,
func() ContactCardChangesCommand {
return ContactCardChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
},
func(path string, rof string) ContactCardGetRefCommand {
return ContactCardGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandContactCardChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp ContactCardChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List },
func(oldState, newState State, hasMoreChanges bool, created, updated []jscontact.ContactCard, destroyed []string) ContactCardChanges {
return ContactCardChanges{
OldState: oldState,
NewState: newState,
HasMoreChanges: hasMoreChanges,
Created: created,
Updated: updated,
Destroyed: destroyed,
}
},
func(resp ContactCardGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
if err != nil {
return ContactCardChanges{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (ContactCardChanges, State, Error) {
result := ContactCardChanges{}
var changes ContactCardChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardChanges, "0", &changes)
if err != nil {
return ContactCardChanges{}, "", err
}
result.NewState = changes.NewState
result.OldState = changes.OldState
result.HasMoreChanges = changes.HasMoreChanges
result.Destroyed = changes.Destroyed
var created ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "1", &created)
if err != nil {
return ContactCardChanges{}, "", err
}
result.Created = created.List
var updated ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "2", &updated)
if err != nil {
return ContactCardChanges{}, "", err
}
result.Updated = updated.List
return result, changes.NewState, nil
})
}
func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR
@@ -253,7 +259,7 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte
if created, ok := setResponse.Created["c"]; !ok || created == nil {
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
logger.Error().Err(berr)
return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
var getResponse ContactCardGetResponse
@@ -265,7 +271,7 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte
if len(getResponse.List) < 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
logger.Error().Err(berr)
return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
return &getResponse.List[0], setResponse.NewState, nil

View File

@@ -56,8 +56,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
logger.Error().Err(err).Send()
return nil, nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return nil, nil, "", "", "", err
}
result, sessionState, state, language, gwerr := command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (getEmailsResult, State, Error) {
if markAsSeen {
@@ -95,8 +94,7 @@ func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context.
get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}}
cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0"))
if err != nil {
logger.Error().Err(err).Send()
return "", "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return "", "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (string, State, Error) {
var response EmailGetResponse
@@ -240,7 +238,7 @@ func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context
invocation(CommandEmailGet, getUpdated, "2"),
)
if err != nil {
return EmailChanges{}, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return EmailChanges{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailChanges, State, Error) {
@@ -536,8 +534,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilter
cmd, err := j.request(session, logger, invocations...)
if err != nil {
logger.Error().Err(err).Send()
return nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryWithSnippetsResult, State, Error) {
@@ -631,7 +628,7 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
invocation(CommandBlobGet, getHash, "1"),
)
if err != nil {
return UploadedEmail{}, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return UploadedEmail{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UploadedEmail, State, Error) {
@@ -650,17 +647,17 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
if len(uploadResponse.Created) != 1 {
logger.Error().Msgf("%T.Created has %v elements instead of 1", uploadResponse, len(uploadResponse.Created))
return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
upload, ok := uploadResponse.Created["0"]
if !ok {
logger.Error().Msgf("%T.Created has no element '0'", uploadResponse)
return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
if len(getResponse.List) != 1 {
logger.Error().Msgf("%T.List has %v elements instead of 1", getResponse, len(getResponse.List))
return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
get := getResponse.List[0]
@@ -714,7 +711,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri
if !ok {
berr := fmt.Errorf("failed to find %s in %s response", string(EmailType), string(CommandEmailSet))
logger.Error().Err(berr)
return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
return created, setResponse.NewState, nil
@@ -886,7 +883,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
return submission, setResponse.NewState, nil
} else {
err = simpleError(fmt.Errorf("failed to submit email: updated is empty"), 0) // TODO proper error handling
err = jmapError(fmt.Errorf("failed to submit email: updated is empty"), 0) // TODO proper error handling
return EmailSubmission{}, "", err
}
})

View File

@@ -9,75 +9,37 @@ import (
)
func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]Identity, SessionState, State, Language, Error) {
logger = j.logger("GetAllIdentities", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, "0"))
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Identity, State, Error) {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response)
if err != nil {
return nil, "", err
}
return response.List, response.State, nil
})
return getTemplate(j, "GetAllIdentities", CommandIdentityGet,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
func(resp IdentityGetResponse) []Identity { return resp.List },
func(resp IdentityGetResponse) State { return resp.State },
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) {
logger = j.logger("GetIdentities", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId, Ids: identityIds}, "0"))
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Identity, State, Error) {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response)
if err != nil {
return nil, "", err
}
return response.List, response.State, nil
})
return getTemplate(j, "GetIdentities", CommandIdentityGet,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: ids}
},
func(resp IdentityGetResponse) []Identity { return resp.List },
func(resp IdentityGetResponse) State { return resp.State },
accountId, session, ctx, logger, acceptLanguage, identityIds,
)
}
type IdentitiesGetResponse struct {
Identities map[string][]Identity `json:"identities,omitempty"`
NotFound []string `json:"notFound,omitempty"`
}
func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, State, Language, Error) {
logger = j.logger("GetIdentitiesForAllAccounts", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
calls := make([]Invocation, len(uniqueAccountIds))
for i, accountId := range uniqueAccountIds {
calls[i] = invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, strconv.Itoa(i))
}
cmd, err := j.request(session, logger, calls...)
if err != nil {
return IdentitiesGetResponse{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentitiesGetResponse, State, Error) {
identities := make(map[string][]Identity, len(uniqueAccountIds))
stateByAccountId := make(map[string]State, len(uniqueAccountIds))
notFound := []string{}
for i, accountId := range uniqueAccountIds {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, strconv.Itoa(i), &response)
if err != nil {
return IdentitiesGetResponse{}, "", err
} else {
identities[accountId] = response.List
}
stateByAccountId[accountId] = response.State
notFound = append(notFound, response.NotFound...)
}
return IdentitiesGetResponse{
Identities: identities,
NotFound: structs.Uniq(notFound),
}, squashState(stateByAccountId), nil
})
func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Identity, SessionState, State, Language, Error) {
return getTemplateN(j, "GetIdentitiesForAllAccounts", CommandIdentityGet,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
func(resp IdentityGetResponse) []Identity { return resp.List },
identity1,
func(resp IdentityGetResponse) State { return resp.State },
accountIds, session, ctx, logger, acceptLanguage, []string{},
)
}
type IdentitiesAndMailboxesGetResponse struct {

View File

@@ -7,7 +7,6 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
type MailboxesResponse struct {
@@ -15,64 +14,32 @@ type MailboxesResponse struct {
NotFound []any `json:"notFound"`
}
// https://jmap.io/spec-mail.html#mailboxget
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, State, Language, Error) {
logger = j.logger("GetMailbox", session, logger)
cmd, err := j.request(session, logger,
invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, "0"),
return getTemplate(j, "GetMailbox", CommandCalendarGet,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId, Ids: ids}
},
func(resp MailboxGetResponse) MailboxesResponse {
return MailboxesResponse{
Mailboxes: resp.List,
NotFound: resp.NotFound,
}
},
func(resp MailboxGetResponse) State { return resp.State },
accountId, session, ctx, logger, acceptLanguage, ids,
)
if err != nil {
return MailboxesResponse{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxesResponse, State, Error) {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "0", &response)
if err != nil {
return MailboxesResponse{}, "", err
}
return MailboxesResponse{
Mailboxes: response.List,
NotFound: response.NotFound,
}, response.State, nil
})
}
func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Mailbox, SessionState, State, Language, Error) {
logger = j.logger("GetAllMailboxes", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return nil, "", "", "", nil
}
invocations := make([]Invocation, n)
for i, accountId := range uniqueAccountIds {
invocations[i] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId}, mcid(accountId, "0"))
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]Mailbox, State, Error) {
resp := map[string][]Mailbox{}
stateByAccountid := map[string]State{}
for _, accountId := range uniqueAccountIds {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "0"), &response)
if err != nil {
return nil, "", err
}
resp[accountId] = response.List
stateByAccountid[accountId] = response.State
}
return resp, squashState(stateByAccountid), nil
})
return getTemplateN(j, "GetAllMailboxes", CommandCalendarGet,
func(accountId string, ids []string) MailboxGetCommand {
return MailboxGetCommand{AccountId: accountId}
},
func(resp MailboxGetResponse) []Mailbox { return resp.List },
identity1,
func(resp MailboxGetResponse) State { return resp.State },
accountIds, session, ctx, logger, acceptLanguage, []string{},
)
}
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) {
@@ -163,154 +130,149 @@ type MailboxChanges struct {
Destroyed []string `json:"destroyed,omitempty"`
}
// 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 string, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) {
logger = j.logger("GetMailboxChanges", session, logger)
changes := MailboxChangesCommand{
AccountId: accountId,
SinceState: sinceState,
MaxChanges: nil,
func newMailboxChanges(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,
}
if maxChanges > 0 {
changes.MaxChanges = &maxChanges
}
getCreated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
}
getUpdated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
}
cmd, err := j.request(session, logger,
invocation(CommandMailboxChanges, changes, "0"),
invocation(CommandMailboxGet, getCreated, "1"),
invocation(CommandMailboxGet, getUpdated, "2"),
)
if err != nil {
return MailboxChanges{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, State, Error) {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, "0", &mailboxResponse)
if err != nil {
return MailboxChanges{}, "", err
}
var createdResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err).Send()
return MailboxChanges{}, "", err
}
var updatedResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
return MailboxChanges{}, "", err
}
return MailboxChanges{
Destroyed: mailboxResponse.Destroyed,
HasMoreChanges: mailboxResponse.HasMoreChanges,
OldState: mailboxResponse.OldState,
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
}, createdResponse.State, nil
})
}
// Retrieve Email changes in Mailboxes of multiple Accounts.
func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, Error) { //NOSONAR
logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context {
sinceStateLogDict := zerolog.Dict()
for k, v := range sinceStateMap {
sinceStateLogDict.Str(log.SafeString(k), log.SafeString(v))
}
return z.Dict(logSinceState, sinceStateLogDict)
})
// 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 changesTemplate(j, "GetMailboxChanges",
CommandMailboxChanges, CommandMailboxGet,
func() MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
},
func(path string, rof string) MailboxGetRefCommand {
return MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandMailboxChanges,
Path: path,
ResultOf: rof,
},
}
},
func(resp MailboxChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp MailboxGetResponse) []Mailbox { return resp.List },
newMailboxChanges,
func(resp MailboxGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
}
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return map[string]MailboxChanges{}, "", "", "", nil
}
// Retrieve Mailbox changes of multiple Accounts.
func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]State, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, Error) { //NOSONAR
return changesTemplateN(j, "GetMailboxChangesForMultipleAccounts",
accountIds, sinceStateMap, CommandMailboxChanges, CommandMailboxGet,
func(accountId string, state State) MailboxChangesCommand {
return MailboxChangesCommand{AccountId: accountId, SinceState: state, MaxChanges: posUIntPtr(maxChanges)}
},
func(accountId string, path string, ref string) MailboxGetRefCommand {
return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: path, ResultOf: ref}}
},
func(resp MailboxChangesResponse) (State, State, bool, []string) {
return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed
},
func(resp MailboxGetResponse) []Mailbox { return resp.List },
newMailboxChanges,
identity1,
func(resp MailboxGetResponse) State { return resp.State },
session, ctx, logger, acceptLanguage,
)
invocations := make([]Invocation, n*3)
for i, accountId := range uniqueAccountIds {
changes := MailboxChangesCommand{
AccountId: accountId,
/*
logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context {
sinceStateLogDict := zerolog.Dict()
for k, v := range sinceStateMap {
sinceStateLogDict.Str(log.SafeString(k), log.SafeString(v))
}
return z.Dict(logSinceState, sinceStateLogDict)
})
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return map[string]MailboxChanges{}, "", "", "", nil
}
sinceState, ok := sinceStateMap[accountId]
if ok {
changes.SinceState = sinceState
}
if maxChanges > 0 {
changes.MaxChanges = &maxChanges
}
getCreated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")},
}
getUpdated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")},
}
invocations[i*3+0] = invocation(CommandMailboxChanges, changes, mcid(accountId, "0"))
invocations[i*3+1] = invocation(CommandMailboxGet, getCreated, mcid(accountId, "1"))
invocations[i*3+2] = invocation(CommandMailboxGet, getUpdated, mcid(accountId, "2"))
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]MailboxChanges, State, Error) {
resp := make(map[string]MailboxChanges, n)
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, mcid(accountId, "0"), &mailboxResponse)
if err != nil {
return nil, "", err
invocations := make([]Invocation, n*3)
for i, accountId := range uniqueAccountIds {
changes := MailboxChangesCommand{
AccountId: accountId,
}
var createdResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &createdResponse)
if err != nil {
return nil, "", err
sinceState, ok := sinceStateMap[accountId]
if ok {
changes.SinceState = sinceState
}
var updatedResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "2"), &updatedResponse)
if err != nil {
return nil, "", err
if maxChanges > 0 {
changes.MaxChanges = &maxChanges
}
resp[accountId] = MailboxChanges{
Destroyed: mailboxResponse.Destroyed,
HasMoreChanges: mailboxResponse.HasMoreChanges,
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
getCreated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")},
}
stateByAccountId[accountId] = createdResponse.State
getUpdated := MailboxGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")},
}
invocations[i*3+0] = invocation(CommandMailboxChanges, changes, mcid(accountId, "0"))
invocations[i*3+1] = invocation(CommandMailboxGet, getCreated, mcid(accountId, "1"))
invocations[i*3+2] = invocation(CommandMailboxGet, getUpdated, mcid(accountId, "2"))
}
return resp, squashState(stateByAccountId), nil
})
cmd, err := j.request(session, logger, invocations...)
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]MailboxChanges, State, Error) {
resp := make(map[string]MailboxChanges, n)
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, mcid(accountId, "0"), &mailboxResponse)
if err != nil {
return nil, "", err
}
var createdResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &createdResponse)
if err != nil {
return nil, "", err
}
var updatedResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "2"), &updatedResponse)
if err != nil {
return nil, "", err
}
resp[accountId] = MailboxChanges{
Destroyed: mailboxResponse.Destroyed,
HasMoreChanges: mailboxResponse.HasMoreChanges,
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: updatedResponse.List,
}
stateByAccountId[accountId] = createdResponse.State
}
return resp, squashState(stateByAccountId), nil
})
*/
}
func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]string, SessionState, State, Language, Error) {
@@ -472,7 +434,7 @@ func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.C
if mailbox, ok := setResp.Created["c"]; ok {
return mailbox, setResp.NewState, nil
} else {
return Mailbox{}, "", simpleError(fmt.Errorf("failed to find created %T in response", Mailbox{}), JmapErrorMissingCreatedObject)
return Mailbox{}, "", jmapError(fmt.Errorf("failed to find created %T in response", Mailbox{}), JmapErrorMissingCreatedObject)
}
})
}

118
pkg/jmap/api_objects.go Normal file
View File

@@ -0,0 +1,118 @@
package jmap
import (
"context"
"github.com/opencloud-eu/opencloud/pkg/log"
)
type Objects struct {
Mailboxes *MailboxGetResponse `json:"mailboxes,omitempty"`
Emails *EmailGetResponse `json:"emails,omitempty"`
Calendars *CalendarGetResponse `json:"calendars,omitempty"`
Events *CalendarEventGetResponse `json:"events,omitempty"`
Addressbooks *AddressBookGetResponse `json:"addressbooks,omitempty"`
Contacts *ContactCardGetResponse `json:"contacts,omitempty"`
}
func (j *Client) GetObjects(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR
mailboxIds []string, emailIds []string,
addressbookIds []string, contactIds []string,
calendarIds []string, eventIds []string,
) (Objects, SessionState, State, Language, Error) {
l := j.logger("GetObjects", session, logger).With()
if len(mailboxIds) > 0 {
l = l.Array("mailboxIds", log.SafeStringArray(mailboxIds))
}
if len(emailIds) > 0 {
l = l.Array("emailIds", log.SafeStringArray(emailIds))
}
if len(addressbookIds) > 0 {
l = l.Array("addressbookIds", log.SafeStringArray(addressbookIds))
}
if len(contactIds) > 0 {
l = l.Array("contactIds", log.SafeStringArray(contactIds))
}
if len(calendarIds) > 0 {
l = l.Array("calendarIds", log.SafeStringArray(calendarIds))
}
if len(eventIds) > 0 {
l = l.Array("eventIds", log.SafeStringArray(eventIds))
}
logger = log.From(l)
methodCalls := []Invocation{}
if len(mailboxIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: mailboxIds}, "mailboxes"))
}
if len(emailIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandEmailGet, EmailGetCommand{AccountId: accountId, Ids: emailIds}, "emails"))
}
if len(addressbookIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandAddressBookGet, AddressBookGetCommand{AccountId: accountId, Ids: addressbookIds}, "addressbooks"))
}
if len(contactIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandContactCardGet, ContactCardGetCommand{AccountId: accountId, Ids: contactIds}, "contacts"))
}
if len(calendarIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandCalendarGet, CalendarGetCommand{AccountId: accountId, Ids: calendarIds}, "calendars"))
}
if len(eventIds) > 0 {
methodCalls = append(methodCalls, invocation(CommandCalendarEventGet, CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}, "events"))
}
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
return Objects{}, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Objects, State, Error) {
objs := Objects{}
var mailboxes MailboxGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandMailboxGet, "mailboxes", &mailboxes); err != nil {
return Objects{}, "", err
} else if ok {
objs.Mailboxes = &mailboxes
}
var emails EmailGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandEmailGet, "emails", &emails); err != nil {
return Objects{}, "", err
} else if ok {
objs.Emails = &emails
}
var calendars CalendarGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandCalendarGet, "calendars", &calendars); err != nil {
return Objects{}, "", err
} else if ok {
objs.Calendars = &calendars
}
var events CalendarEventGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandCalendarEventGet, "events", &events); err != nil {
return Objects{}, "", err
} else if ok {
objs.Events = &events
}
var addressbooks AddressBookGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandAddressBookGet, "addressbooks", &addressbooks); err != nil {
return Objects{}, "", err
} else if ok {
objs.Addressbooks = &addressbooks
}
var contacts ContactCardGetResponse
if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandContactCardGet, "contacts", &contacts); err != nil {
return Objects{}, "", err
} else if ok {
objs.Contacts = &contacts
}
state := squashStates(mailboxes.State, emails.State, calendars.State, events.State, addressbooks.State, contacts.State)
return objs, state, nil
})
}

View File

@@ -12,6 +12,7 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co
return QuotaGetCommand{AccountId: accountId}
},
identity1,
identity1,
func(resp QuotaGetResponse) State { return resp.State },
accountIds, session, ctx, logger, acceptLanguage, []string{},
)

View File

@@ -94,7 +94,7 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse
if len(getResponse.List) != 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(VacationResponseType), string(CommandVacationResponseGet))
logger.Error().Msg(berr.Error())
return VacationResponse{}, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return VacationResponse{}, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
return getResponse.List[0], setResponse.NewState, nil

View File

@@ -79,11 +79,11 @@ func (j *Client) loggerParams(operation string, _ *Session, logger *log.Logger,
func (j *Client) maxCallsCheck(calls int, session *Session, logger *log.Logger) Error {
if calls > session.Capabilities.Core.MaxCallsInRequest {
logger.Warn().
logger.Error().
Int("max-calls-in-request", session.Capabilities.Core.MaxCallsInRequest).
Int("calls-in-request", calls).
Msgf("number of calls in request payload (%d) would exceed the allowed maximum (%d)", session.Capabilities.Core.MaxCallsInRequest, calls)
return simpleError(errTooManyMethodCalls, JmapErrorTooManyMethodCalls)
Msgf("number of calls in request payload (%d) exceeds the allowed maximum (%d)", session.Capabilities.Core.MaxCallsInRequest, calls)
return jmapError(errTooManyMethodCalls, JmapErrorTooManyMethodCalls)
}
return nil
}

View File

@@ -38,6 +38,7 @@ const (
JmapErrorWssFailedToRetrieveSession
JmapErrorSocketPushUnsupported
JmapErrorMissingCreatedObject
JmapInvalidObjectState
)
var (
@@ -49,35 +50,52 @@ type Error interface {
error
}
type SimpleError struct {
code int
err error
type JmapError struct {
code int
err error
typ string
description string
}
var _ Error = &SimpleError{}
var _ Error = &JmapError{}
func (e SimpleError) Code() int {
func (e JmapError) Code() int {
return e.code
}
func (e SimpleError) Unwrap() error {
func (e JmapError) Unwrap() error {
return e.err
}
func (e SimpleError) Error() string {
func (e JmapError) Error() string {
if e.err != nil {
return e.err.Error()
} else {
return ""
}
}
func (e JmapError) Type() string {
return e.typ
}
func (e JmapError) Description() string {
return e.description
}
func simpleError(err error, code int) Error {
func jmapError(err error, code int) Error {
if err != nil {
return SimpleError{code: code, err: err}
return JmapError{code: code, err: err}
} else {
return nil
}
}
func jmapResponseError(code int, err error, typ string, description string) JmapError {
return JmapError{
code: code,
err: err,
typ: typ,
description: description,
}
}
func setErrorError(err SetError, objectType ObjectType) Error {
var e error
if len(err.Properties) > 0 {
@@ -85,5 +103,5 @@ func setErrorError(err SetError, objectType ObjectType) Error {
} else {
e = fmt.Errorf("failed to modify %s due to %s error: %s", objectType, err.Type, err.Description)
}
return SimpleError{code: JmapErrorSetError, err: e}
return JmapError{code: JmapErrorSetError, err: e}
}

View File

@@ -155,7 +155,7 @@ var (
func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
if sessionUrl == nil {
logger.Error().Msg("sessionUrl is nil")
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: errNilBaseUrl}
return SessionResponse{}, jmapError(errNilBaseUrl, JmapErrorInvalidHttpRequest)
}
// See the JMAP specification on Service Autodiscovery: https://jmap.io/spec-core.html#service-autodiscovery
// There are two standardised autodiscovery methods in use for Internet protocols:
@@ -170,7 +170,7 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
req, err := http.NewRequest(http.MethodGet, sessionUrlStr, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", sessionUrl)
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
return SessionResponse{}, jmapError(err, JmapErrorInvalidHttpRequest)
}
if err := h.auth(ctx, username, logger, req); err != nil {
return SessionResponse{}, err
@@ -181,12 +181,12 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
if err != nil {
h.listener.OnFailedRequest(endpoint, err)
logger.Error().Err(err).Msgf("failed to perform GET %v", sessionUrl)
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
return SessionResponse{}, jmapError(err, JmapErrorInvalidHttpRequest)
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
return SessionResponse{}, jmapError(fmt.Errorf("JMAP API response status is %v", res.Status), JmapErrorServerResponse)
}
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
@@ -203,7 +203,7 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
if err != nil {
logger.Error().Err(err).Msg("failed to read response body") //NOSONAR
h.listener.OnResponseBodyReadingError(endpoint, err)
return SessionResponse{}, SimpleError{code: JmapErrorReadingResponseBody, err: err}
return SessionResponse{}, jmapError(err, JmapErrorReadingResponseBody)
}
var data SessionResponse
@@ -211,7 +211,7 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us
if err != nil {
logger.Error().Str(logHttpUrl, log.SafeString(sessionUrlStr)).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
return SessionResponse{}, jmapError(err, JmapErrorDecodingResponseBody)
}
return data, nil
@@ -225,13 +225,13 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
bodyBytes, err := json.Marshal(request)
if err != nil {
logger.Error().Err(err).Msg("failed to marshall JSON payload")
return nil, "", SimpleError{code: JmapErrorEncodingRequestBody, err: err}
return nil, "", jmapError(err, JmapErrorEncodingRequestBody)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl)
return nil, "", SimpleError{code: JmapErrorCreatingRequest, err: err}
return nil, "", jmapError(err, JmapErrorCreatingRequest)
}
// Some JMAP APIs use the Accept-Language header to determine which language to use to translate
@@ -257,7 +257,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
if err != nil {
h.listener.OnFailedRequest(endpoint, err)
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
return nil, "", SimpleError{code: JmapErrorSendingRequest, err: err}
return nil, "", jmapError(err, JmapErrorSendingRequest)
}
if logger.Trace().Enabled() {
@@ -273,7 +273,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") //NOSONAR
return nil, language, SimpleError{code: JmapErrorServerResponse, err: err}
return nil, language, jmapError(err, JmapErrorServerResponse)
}
if res.Body != nil {
defer func(Body io.ReadCloser) {
@@ -289,7 +289,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
h.listener.OnResponseBodyReadingError(endpoint, err)
return nil, language, SimpleError{code: JmapErrorServerResponse, err: err}
return nil, language, jmapError(err, JmapErrorServerResponse)
}
return body, language, nil
@@ -301,7 +301,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, body)
if err != nil {
logger.Error().Err(err).Msgf("failed to create POST request for %v", uploadUrl)
return UploadedBlob{}, "", SimpleError{code: JmapErrorCreatingRequest, err: err}
return UploadedBlob{}, "", jmapError(err, JmapErrorCreatingRequest)
}
req.Header.Add("Content-Type", contentType)
req.Header.Add("User-Agent", h.userAgent)
@@ -323,7 +323,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
if err != nil {
h.listener.OnFailedRequest(endpoint, err)
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
return UploadedBlob{}, "", SimpleError{code: JmapErrorSendingRequest, err: err}
return UploadedBlob{}, "", jmapError(err, JmapErrorSendingRequest)
}
if logger.Trace().Enabled() {
responseBytes, err := httputil.DumpResponse(res, true)
@@ -338,7 +338,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return UploadedBlob{}, language, SimpleError{code: JmapErrorServerResponse, err: err}
return UploadedBlob{}, language, jmapError(err, JmapErrorServerResponse)
}
if res.Body != nil {
defer func(Body io.ReadCloser) {
@@ -354,7 +354,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
if err != nil {
logger.Error().Err(err).Msg("failed to read response body")
h.listener.OnResponseBodyReadingError(endpoint, err)
return UploadedBlob{}, language, SimpleError{code: JmapErrorServerResponse, err: err}
return UploadedBlob{}, language, jmapError(err, JmapErrorServerResponse)
}
logger.Trace()
@@ -364,7 +364,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
if err != nil {
logger.Error().Str(logHttpUrl, log.SafeString(uploadUrl)).Err(err).Msg("failed to decode JSON payload from the upload response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
return UploadedBlob{}, language, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
return UploadedBlob{}, language, jmapError(err, JmapErrorDecodingResponseBody)
}
return result, language, nil
@@ -376,7 +376,7 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil)
if err != nil {
logger.Error().Err(err).Msgf("failed to create GET request for %v", downloadUrl)
return nil, "", SimpleError{code: JmapErrorCreatingRequest, err: err}
return nil, "", jmapError(err, JmapErrorCreatingRequest)
}
req.Header.Add("User-Agent", h.userAgent)
if acceptLanguage != "" {
@@ -397,7 +397,7 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
if err != nil {
h.listener.OnFailedRequest(endpoint, err)
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
return nil, "", SimpleError{code: JmapErrorSendingRequest, err: err}
return nil, "", jmapError(err, JmapErrorSendingRequest)
}
if logger.Trace().Enabled() {
responseBytes, err := httputil.DumpResponse(res, false)
@@ -414,7 +414,7 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return nil, language, SimpleError{code: JmapErrorServerResponse, err: err}
return nil, language, jmapError(err, JmapErrorServerResponse)
}
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
@@ -502,14 +502,14 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func(
session, err := sessionProvider()
if err != nil {
return nil, "", "", SimpleError{code: JmapErrorWssFailedToRetrieveSession, err: err}
return nil, "", "", jmapError(err, JmapErrorWssFailedToRetrieveSession)
}
if session == nil {
return nil, "", "", SimpleError{code: JmapErrorWssFailedToRetrieveSession, err: nil}
return nil, "", "", jmapError(fmt.Errorf("WSS connection failed to retrieve JMAP session"), JmapErrorWssFailedToRetrieveSession)
}
if !session.SupportsWebsocketPush {
return nil, "", "", SimpleError{code: JmapErrorSocketPushUnsupported, err: nil}
return nil, "", "", jmapError(fmt.Errorf("WSS connection returned a session that does not support websocket push"), JmapErrorSocketPushUnsupported)
}
username := session.Username
@@ -520,7 +520,7 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func(
w.auth(ctx, username, logger, h)
c, res, err := w.dialer.DialContext(ctx, u.String(), h)
if err != nil {
return nil, "", endpoint, SimpleError{code: JmapErrorFailedToEstablishWssConnection, err: err}
return nil, "", endpoint, jmapError(err, JmapErrorFailedToEstablishWssConnection)
}
if w.logger.Trace().Enabled() {
@@ -535,7 +535,7 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func(
if res.StatusCode != 101 {
w.eventListener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 101")
return nil, "", endpoint, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP WS API response status is %v", res.Status)}
return nil, "", endpoint, jmapError(fmt.Errorf("JMAP WS API response status is %v", res.Status), JmapErrorServerResponse)
} else {
w.eventListener.OnSuccessfulWsRequest(endpoint, res.StatusCode)
}
@@ -544,7 +544,7 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func(
// The reply from the server MUST also contain a corresponding "Sec-WebSocket-Protocol" header
// field with a value of "jmap" in order for a JMAP subprotocol connection to be established.
if !slices.Contains(res.Header.Values("Sec-WebSocket-Protocol"), "jmap") {
return nil, "", endpoint, SimpleError{code: JmapErrorWssConnectionResponseMissingJmapSubprotocol}
return nil, "", endpoint, jmapError(fmt.Errorf("WSS connection header does not contain Sec-WebSocket-Protocol:jmap"), JmapErrorWssConnectionResponseMissingJmapSubprotocol)
}
return c, username, endpoint, nil
@@ -625,14 +625,14 @@ func (w *HttpWsClientFactory) EnableNotifications(ctx context.Context, pushState
data, err := json.Marshal(msg)
if err != nil {
return nil, SimpleError{code: JmapErrorWssFailedToSendWebSocketPushEnable, err: err}
return nil, jmapError(err, JmapErrorWssFailedToSendWebSocketPushEnable)
}
if w.logger.Trace().Enabled() {
w.logger.Trace().Str(logEndpoint, endpoint).Str(logProto, logProtoJmapWs).Str(logType, logTypeRequest).Msg(string(data))
}
if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
return nil, SimpleError{code: JmapErrorWssFailedToSendWebSocketPushEnable, err: err}
return nil, jmapError(err, JmapErrorWssFailedToSendWebSocketPushEnable)
}
wsc := &HttpWsClient{
@@ -664,13 +664,13 @@ func (c *HttpWsClient) DisableNotifications() Error {
cerr := c.c.Close()
if werr != nil {
return SimpleError{code: JmapErrorWssFailedToClose, err: werr}
return jmapError(werr, JmapErrorWssFailedToClose)
}
if merr != nil {
return SimpleError{code: JmapErrorWssFailedToClose, err: merr}
return jmapError(merr, JmapErrorWssFailedToClose)
}
if cerr != nil {
return SimpleError{code: JmapErrorWssFailedToClose, err: cerr}
return jmapError(cerr, JmapErrorWssFailedToClose)
}
return nil
}

View File

@@ -912,6 +912,7 @@ var basicColors = []string{
"aqua",
}
/*
// https://www.w3.org/TR/SVG11/types.html#ColorKeywords
var extendedColors = []string{
"aliceblue",
@@ -1062,6 +1063,7 @@ var extendedColors = []string{
"yellow",
"yellowgreen",
}
*/
func propmap[T any](enabled bool, min int, max int, container map[string]any, name string, cardProperty *map[string]T, generator func(int, string) (map[string]any, T, error)) error {
if !enabled {

View File

@@ -1343,7 +1343,7 @@ type MailboxChangesCommand struct {
// This is the string that was returned as the state argument in the Mailbox/get response.
//
// The server will return the changes that have occurred since this state.
SinceState string `json:"sinceState,omitempty"`
SinceState State `json:"sinceState,omitempty"`
// The maximum number of ids to return in the response.
//
@@ -4960,6 +4960,11 @@ type AddressBookGetCommand struct {
Ids []string `json:"ids,omitempty"`
}
type AddressBookGetRefCommand struct {
AccountId string `json:"accountId"`
IdsRef *ResultReference `json:"#ids,omitempty"`
}
type AddressBookGetResponse struct {
AccountId string `json:"accountId"`
State State `json:"state,omitempty"`
@@ -4967,6 +4972,54 @@ type AddressBookGetResponse struct {
NotFound []string `json:"notFound,omitempty"`
}
type AddressBookChangesCommand struct {
// The id of the account to use.
AccountId string `json:"accountId"`
// The current state of the client.
//
// This is the string that was returned as the state argument in the AddressBook/get response.
//
// The server will return the changes that have occurred since this state.
SinceState State `json:"sinceState,omitempty"`
// The maximum number of ids to return in the response.
//
// The server MAY choose to return fewer than this value but MUST NOT return more.
//
// If not given by the client, the server may choose how many to return.
//
// If supplied by the client, the value MUST be a positive integer greater than 0.
//
// If a value outside of this range is given, the server MUST reject the call with an invalidArguments error.
MaxChanges *uint `json:"maxChanges,omitzero"`
}
type AddressBookChangesResponse struct {
// The id of the account used for the call.
AccountId string `json:"accountId"`
// This is the sinceState argument echoed back; its the state from which the server is returning changes.
OldState State `json:"oldState"`
// This is the state the client will be in after applying the set of changes to the old state.
NewState State `json:"newState"`
// If true, the client may call Mailbox/changes again with the newState returned to get further updates.
//
// If false, newState is the current server state.
HasMoreChanges bool `json:"hasMoreChanges"`
// An array of ids for records that have been created since the old state.
Created []string `json:"created,omitempty"`
// An array of ids for records that have been updated since the old state.
Updated []string `json:"updated,omitempty"`
// An array of ids for records that have been destroyed since the old state.
Destroyed []string `json:"destroyed,omitempty"`
}
type ContactCardComparator struct {
// The name of the property on the objects to compare.
Property string `json:"property,omitempty"`
@@ -5326,7 +5379,7 @@ type ContactCardChangesCommand struct {
// The current state of the client.
// This is the string that was returned as the "state" argument in the "ContactCard/get" response.
// The server will return the changes that have occurred since this state.
SinceState string `json:"sinceState,omitempty"`
SinceState State `json:"sinceState,omitempty"`
// The maximum number of ids to return in the response.
// The server MAY choose to return fewer than this value but MUST NOT return more.
@@ -5496,6 +5549,11 @@ type CalendarGetCommand struct {
Ids []string `json:"ids,omitempty"`
}
type CalendarGetRefCommand struct {
AccountId string `json:"accountId"`
IdsRef *ResultReference `json:"#ids,omitempty"`
}
type CalendarGetResponse struct {
AccountId string `json:"accountId"`
State State `json:"state,omitempty"`
@@ -5503,6 +5561,53 @@ type CalendarGetResponse struct {
NotFound []string `json:"notFound,omitempty"`
}
type CalendarChangesCommand struct {
// The id of the account to use.
AccountId string `json:"accountId"`
// The current state of the client.
//
// This is the string that was returned as the state argument in the Calendar/get response.
//
// The server will return the changes that have occurred since this state.
SinceState State `json:"sinceState,omitempty"`
// The maximum number of ids to return in the response.
//
// The server MAY choose to return fewer than this value but MUST NOT return more.
//
// If not given by the client, the server may choose how many to return.
//
// If supplied by the client, the value MUST be a positive integer greater than 0.
//
// If a value outside of this range is given, the server MUST reject the call with an invalidArguments error.
MaxChanges *uint `json:"maxChanges,omitzero"`
}
type CalendarChangesResponse struct {
// The id of the account used for the call.
AccountId string `json:"accountId"`
// This is the "sinceState" argument echoed back; it's the state from which the server is returning changes.
OldState State `json:"oldState"`
// This is the state the client will be in after applying the set of changes to the old state.
NewState State `json:"newState"`
// If true, the client may call "Calendar/changes" again with the "newState" returned to get further updates.
// If false, "newState" is the current server state.
HasMoreChanges bool `json:"hasMoreChanges"`
// An array of ids for records that have been created since the old state.
Created []string `json:"created,omitempty"`
// An array of ids for records that have been updated since the old state.
Updated []string `json:"updated,omitempty"`
// An array of ids for records that have been destroyed since the old state.
Destroyed []string `json:"destroyed,omitempty"`
}
type CalendarEventComparator struct {
// The name of the property on the objects to compare.
Property string `json:"property,omitempty"`
@@ -5807,6 +5912,53 @@ type CalendarEventGetResponse struct {
NotFound []any `json:"notFound"`
}
type CalendarEventChangesCommand struct {
// The id of the account to use.
AccountId string `json:"accountId"`
// The current state of the client.
//
// This is the string that was returned as the state argument in the CalendarEvent/get response.
//
// The server will return the changes that have occurred since this state.
SinceState State `json:"sinceState,omitempty"`
// The maximum number of ids to return in the response.
//
// The server MAY choose to return fewer than this value but MUST NOT return more.
//
// If not given by the client, the server may choose how many to return.
//
// If supplied by the client, the value MUST be a positive integer greater than 0.
//
// If a value outside of this range is given, the server MUST reject the call with an invalidArguments error.
MaxChanges *uint `json:"maxChanges,omitzero"`
}
type CalendarEventChangesResponse struct {
// The id of the account used for the call.
AccountId string `json:"accountId"`
// This is the "sinceState" argument echoed back; it's the state from which the server is returning changes.
OldState State `json:"oldState"`
// This is the state the client will be in after applying the set of changes to the old state.
NewState State `json:"newState"`
// If true, the client may call "CalendarEvent/changes" again with the "newState" returned to get further updates.
// If false, "newState" is the current server state.
HasMoreChanges bool `json:"hasMoreChanges"`
// An array of ids for records that have been created since the old state.
Created []string `json:"created,omitempty"`
// An array of ids for records that have been updated since the old state.
Updated []string `json:"updated,omitempty"`
// An array of ids for records that have been destroyed since the old state.
Destroyed []string `json:"destroyed,omitempty"`
}
type CalendarEventUpdate map[string]any
type CalendarEventSetCommand struct {
@@ -5915,68 +6067,74 @@ type ErrorResponse struct {
}
const (
ErrorCommand Command = "error" // only occurs in responses
CommandBlobGet Command = "Blob/get"
CommandBlobUpload Command = "Blob/upload"
CommandEmailGet Command = "Email/get"
CommandEmailQuery Command = "Email/query"
CommandEmailChanges Command = "Email/changes"
CommandEmailSet Command = "Email/set"
CommandEmailImport Command = "Email/import"
CommandEmailSubmissionGet Command = "EmailSubmission/get"
CommandEmailSubmissionSet Command = "EmailSubmission/set"
CommandThreadGet Command = "Thread/get"
CommandMailboxGet Command = "Mailbox/get"
CommandMailboxSet Command = "Mailbox/set"
CommandMailboxQuery Command = "Mailbox/query"
CommandMailboxChanges Command = "Mailbox/changes"
CommandIdentityGet Command = "Identity/get"
CommandIdentitySet Command = "Identity/set"
CommandVacationResponseGet Command = "VacationResponse/get"
CommandVacationResponseSet Command = "VacationResponse/set"
CommandSearchSnippetGet Command = "SearchSnippet/get"
CommandQuotaGet Command = "Quota/get"
CommandAddressBookGet Command = "AddressBook/get"
CommandContactCardQuery Command = "ContactCard/query"
CommandContactCardGet Command = "ContactCard/get"
CommandContactCardChanges Command = "ContactCard/changes"
CommandContactCardSet Command = "ContactCard/set"
CommandCalendarEventParse Command = "CalendarEvent/parse"
CommandCalendarGet Command = "Calendar/get"
CommandCalendarEventQuery Command = "CalendarEvent/query"
CommandCalendarEventGet Command = "CalendarEvent/get"
CommandCalendarEventSet Command = "CalendarEvent/set"
ErrorCommand Command = "error" // only occurs in responses
CommandBlobGet Command = "Blob/get"
CommandBlobUpload Command = "Blob/upload"
CommandEmailGet Command = "Email/get"
CommandEmailQuery Command = "Email/query"
CommandEmailChanges Command = "Email/changes"
CommandEmailSet Command = "Email/set"
CommandEmailImport Command = "Email/import"
CommandEmailSubmissionGet Command = "EmailSubmission/get"
CommandEmailSubmissionSet Command = "EmailSubmission/set"
CommandThreadGet Command = "Thread/get"
CommandMailboxGet Command = "Mailbox/get"
CommandMailboxSet Command = "Mailbox/set"
CommandMailboxQuery Command = "Mailbox/query"
CommandMailboxChanges Command = "Mailbox/changes"
CommandIdentityGet Command = "Identity/get"
CommandIdentitySet Command = "Identity/set"
CommandVacationResponseGet Command = "VacationResponse/get"
CommandVacationResponseSet Command = "VacationResponse/set"
CommandSearchSnippetGet Command = "SearchSnippet/get"
CommandQuotaGet Command = "Quota/get"
CommandAddressBookGet Command = "AddressBook/get"
CommandAddressBookChanges Command = "AddressBook/changes"
CommandContactCardQuery Command = "ContactCard/query"
CommandContactCardGet Command = "ContactCard/get"
CommandContactCardChanges Command = "ContactCard/changes"
CommandContactCardSet Command = "ContactCard/set"
CommandCalendarEventParse Command = "CalendarEvent/parse"
CommandCalendarGet Command = "Calendar/get"
CommandCalendarChanges Command = "Calendar/changes"
CommandCalendarEventQuery Command = "CalendarEvent/query"
CommandCalendarEventGet Command = "CalendarEvent/get"
CommandCalendarEventSet Command = "CalendarEvent/set"
CommandCalendarEventChanges Command = "CalendarEvent/changes"
)
var CommandResponseTypeMap = map[Command]func() any{
ErrorCommand: func() any { return ErrorResponse{} },
CommandBlobGet: func() any { return BlobGetResponse{} },
CommandBlobUpload: func() any { return BlobUploadResponse{} },
CommandMailboxQuery: func() any { return MailboxQueryResponse{} },
CommandMailboxGet: func() any { return MailboxGetResponse{} },
CommandMailboxSet: func() any { return MailboxSetResponse{} },
CommandMailboxChanges: func() any { return MailboxChangesResponse{} },
CommandEmailQuery: func() any { return EmailQueryResponse{} },
CommandEmailChanges: func() any { return EmailChangesResponse{} },
CommandEmailGet: func() any { return EmailGetResponse{} },
CommandEmailSet: func() any { return EmailSetResponse{} },
CommandEmailSubmissionGet: func() any { return EmailSubmissionGetResponse{} },
CommandEmailSubmissionSet: func() any { return EmailSubmissionSetResponse{} },
CommandThreadGet: func() any { return ThreadGetResponse{} },
CommandIdentityGet: func() any { return IdentityGetResponse{} },
CommandIdentitySet: func() any { return IdentitySetResponse{} },
CommandVacationResponseGet: func() any { return VacationResponseGetResponse{} },
CommandVacationResponseSet: func() any { return VacationResponseSetResponse{} },
CommandSearchSnippetGet: func() any { return SearchSnippetGetResponse{} },
CommandQuotaGet: func() any { return QuotaGetResponse{} },
CommandAddressBookGet: func() any { return AddressBookGetResponse{} },
CommandContactCardQuery: func() any { return ContactCardQueryResponse{} },
CommandContactCardGet: func() any { return ContactCardGetResponse{} },
CommandContactCardChanges: func() any { return ContactCardChangesResponse{} },
CommandContactCardSet: func() any { return ContactCardSetResponse{} },
CommandCalendarEventParse: func() any { return CalendarEventParseResponse{} },
CommandCalendarGet: func() any { return CalendarGetResponse{} },
CommandCalendarEventQuery: func() any { return CalendarEventQueryResponse{} },
CommandCalendarEventGet: func() any { return CalendarEventGetResponse{} },
CommandCalendarEventSet: func() any { return CalendarEventSetResponse{} },
ErrorCommand: func() any { return ErrorResponse{} },
CommandBlobGet: func() any { return BlobGetResponse{} },
CommandBlobUpload: func() any { return BlobUploadResponse{} },
CommandMailboxQuery: func() any { return MailboxQueryResponse{} },
CommandMailboxGet: func() any { return MailboxGetResponse{} },
CommandMailboxSet: func() any { return MailboxSetResponse{} },
CommandMailboxChanges: func() any { return MailboxChangesResponse{} },
CommandEmailQuery: func() any { return EmailQueryResponse{} },
CommandEmailChanges: func() any { return EmailChangesResponse{} },
CommandEmailGet: func() any { return EmailGetResponse{} },
CommandEmailSet: func() any { return EmailSetResponse{} },
CommandEmailSubmissionGet: func() any { return EmailSubmissionGetResponse{} },
CommandEmailSubmissionSet: func() any { return EmailSubmissionSetResponse{} },
CommandThreadGet: func() any { return ThreadGetResponse{} },
CommandIdentityGet: func() any { return IdentityGetResponse{} },
CommandIdentitySet: func() any { return IdentitySetResponse{} },
CommandVacationResponseGet: func() any { return VacationResponseGetResponse{} },
CommandVacationResponseSet: func() any { return VacationResponseSetResponse{} },
CommandSearchSnippetGet: func() any { return SearchSnippetGetResponse{} },
CommandQuotaGet: func() any { return QuotaGetResponse{} },
CommandAddressBookGet: func() any { return AddressBookGetResponse{} },
CommandAddressBookChanges: func() any { return AddressBookChangesResponse{} },
CommandContactCardQuery: func() any { return ContactCardQueryResponse{} },
CommandContactCardGet: func() any { return ContactCardGetResponse{} },
CommandContactCardChanges: func() any { return ContactCardChangesResponse{} },
CommandContactCardSet: func() any { return ContactCardSetResponse{} },
CommandCalendarEventParse: func() any { return CalendarEventParseResponse{} },
CommandCalendarGet: func() any { return CalendarGetResponse{} },
CommandCalendarChanges: func() any { return CalendarChangesResponse{} },
CommandCalendarEventQuery: func() any { return CalendarEventQueryResponse{} },
CommandCalendarEventGet: func() any { return CalendarEventGetResponse{} },
CommandCalendarEventSet: func() any { return CalendarEventSetResponse{} },
CommandCalendarEventChanges: func() any { return CalendarEventChangesResponse{} },
}

View File

@@ -1817,3 +1817,50 @@ func (e Exemplar) EmailChanges() EmailChanges {
Destroyed: []string{"mmnan", "moxzz"},
}
}
func (e Exemplar) Changes() Changes {
return Changes{
MaxChanges: 3,
Mailboxes: &MailboxChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "rafrag",
HasMoreChanges: true,
Created: []string{"d", "e", "a"},
},
Emails: &EmailChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "rafrag",
HasMoreChanges: true,
Created: []string{"bmaaaaal", "hqaaaab2", "hqaaaab0"},
},
Calendars: &CalendarChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "sci",
HasMoreChanges: false,
Created: []string{"b"},
},
Events: &CalendarEventChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "sci",
HasMoreChanges: false,
},
Addressbooks: &AddressBookChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "sb2",
HasMoreChanges: false,
Created: []string{"b", "c"},
},
Contacts: &ContactCardChangesResponse{
AccountId: e.AccountId,
OldState: "n",
NewState: "rbsxqeay",
HasMoreChanges: true,
Created: []string{"fq", "fr", "fs"},
},
}
}

View File

@@ -56,12 +56,12 @@ type Session struct {
}
var (
invalidSessionResponseErrorMissingUsername = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide a username")}
invalidSessionResponseErrorMissingApiUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide an API URL")}
invalidSessionResponseErrorInvalidApiUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response provides an invalid API URL")}
invalidSessionResponseErrorMissingUploadUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide an upload URL")}
invalidSessionResponseErrorMissingDownloadUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response does not provide a download URL")}
invalidSessionResponseErrorInvalidWebsocketUrl = SimpleError{code: JmapErrorInvalidSessionResponse, err: errors.New("JMAP session response provides an invalid Websocket URL")}
invalidSessionResponseErrorMissingUsername = jmapError(errors.New("JMAP session response does not provide a username"), JmapErrorInvalidSessionResponse)
invalidSessionResponseErrorMissingApiUrl = jmapError(errors.New("JMAP session response does not provide an API URL"), JmapErrorInvalidSessionResponse)
invalidSessionResponseErrorInvalidApiUrl = jmapError(errors.New("JMAP session response provides an invalid API URL"), JmapErrorInvalidSessionResponse)
invalidSessionResponseErrorMissingUploadUrl = jmapError(errors.New("JMAP session response does not provide an upload URL"), JmapErrorInvalidSessionResponse)
invalidSessionResponseErrorMissingDownloadUrl = jmapError(errors.New("JMAP session response does not provide a download URL"), JmapErrorInvalidSessionResponse)
invalidSessionResponseErrorInvalidWebsocketUrl = jmapError(errors.New("JMAP session response provides an invalid Websocket URL"), JmapErrorInvalidSessionResponse)
)
// Create a new Session from a SessionResponse.

View File

@@ -6,6 +6,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR
@@ -36,10 +37,11 @@ func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR
})
}
func getTemplateN[GETREQ any, GETRESP any, RESP any]( //NOSONAR
func getTemplateN[GETREQ any, GETRESP any, ITEM any, RESP any]( //NOSONAR
client *Client, name string, getCommand Command,
getCommandFactory func(string, []string) GETREQ,
mapper func(map[string]GETRESP) RESP,
itemMapper func(GETRESP) ITEM,
respMapper func(map[string]ITEM) RESP,
stateMapper func(GETRESP) State,
accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
@@ -59,16 +61,18 @@ func getTemplateN[GETREQ any, GETRESP any, RESP any]( //NOSONAR
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
result := map[string]GETRESP{}
result := map[string]ITEM{}
responses := map[string]GETRESP{}
for _, accountId := range uniqueAccountIds {
var response GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "0"), &response)
var resp GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "0"), &resp)
if err != nil {
return zero, "", err
}
result[accountId] = response
responses[accountId] = resp
result[accountId] = itemMapper(resp)
}
return mapper(result), squashStateFunc(result, stateMapper), nil
return respMapper(result), squashStateFunc(responses, stateMapper), nil
})
}
@@ -110,7 +114,7 @@ func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( //
if created, ok := createdMap["c"]; !ok || created == nil {
berr := fmt.Errorf("failed to find %s in %s response", string(t), string(setCommand))
logger.Error().Err(berr)
return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
var getResponse GETRESP
@@ -124,7 +128,7 @@ func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( //
if len(list) < 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(t), string(getCommand))
logger.Error().Err(berr)
return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
return &list[0], stateMapper(setResponse), nil
@@ -154,3 +158,141 @@ func deleteTemplate[REQ any, RESP any](client *Client, name string, c Command, /
return notDestroyedMapper(setResponse), stateMapper(setResponse), nil
})
}
func changesTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, RESP any]( //NOSONAR
client *Client, name string,
changesCommand Command, getCommand Command,
changesCommandFactory func() CHANGESREQ,
getCommandFactory func(string, string) GETREQ,
changesMapper func(CHANGESRESP) (State, State, bool, []string),
getMapper func(GETRESP) []ITEM,
respMapper func(State, State, bool, []ITEM, []ITEM, []string) RESP,
stateMapper func(GETRESP) State,
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
var zero RESP
changes := changesCommandFactory()
getCreated := getCommandFactory("/created", "0") //NOSONAR
getUpdated := getCommandFactory("/updated", "0") //NOSONAR
cmd, err := client.request(session, logger,
invocation(changesCommand, changes, "0"),
invocation(getCommand, getCreated, "1"),
invocation(getCommand, getUpdated, "2"),
)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(logger, body, changesCommand, "0", &changesResponse)
if err != nil {
return zero, "", err
}
var createdResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &createdResponse)
if err != nil {
logger.Error().Err(err).Send()
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
return zero, "", err
}
oldState, newState, hasMoreChanges, destroyed := changesMapper(changesResponse)
created := getMapper(createdResponse)
updated := getMapper(updatedResponse)
result := respMapper(oldState, newState, hasMoreChanges, created, updated, destroyed)
return result, stateMapper(createdResponse), nil
})
}
func changesTemplateN[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, CHANGESITEM any, RESP any]( //NOSONAR
client *Client, name string,
accountIds []string, sinceStateMap map[string]State,
changesCommand Command, getCommand Command,
changesCommandFactory func(string, State) CHANGESREQ,
getCommandFactory func(string, string, string) GETREQ,
changesMapper func(CHANGESRESP) (State, State, bool, []string),
getMapper func(GETRESP) []ITEM,
changesItemMapper func(State, State, bool, []ITEM, []ITEM, []string) CHANGESITEM,
respMapper func(map[string]CHANGESITEM) RESP,
stateMapper func(GETRESP) State,
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
logger = client.loggerParams(name, session, logger, func(z zerolog.Context) zerolog.Context {
sinceStateLogDict := zerolog.Dict()
for k, v := range sinceStateMap {
sinceStateLogDict.Str(log.SafeString(k), log.SafeString(string(v)))
}
return z.Dict(logSinceState, sinceStateLogDict)
})
var zero RESP
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return zero, "", "", "", nil
}
invocations := make([]Invocation, n*3)
for i, accountId := range uniqueAccountIds {
sinceState, ok := sinceStateMap[accountId]
if !ok {
sinceState = ""
}
changes := changesCommandFactory(accountId, sinceState)
ref := mcid(accountId, "0")
getCreated := getCommandFactory(accountId, "/created", ref)
getUpdated := getCommandFactory(accountId, "/updated", ref)
invocations[i*3+0] = invocation(changesCommand, changes, ref)
invocations[i*3+1] = invocation(getCommand, getCreated, mcid(accountId, "1"))
invocations[i*3+2] = invocation(getCommand, getUpdated, mcid(accountId, "2"))
}
cmd, err := client.request(session, logger, invocations...)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
changesItemByAccount := make(map[string]CHANGESITEM, n)
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(logger, body, changesCommand, mcid(accountId, "0"), &changesResponse)
if err != nil {
return zero, "", err
}
var createdResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "1"), &createdResponse)
if err != nil {
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "2"), &updatedResponse)
if err != nil {
return zero, "", err
}
oldState, newState, hasMoreChanges, destroyed := changesMapper(changesResponse)
created := getMapper(createdResponse)
updated := getMapper(updatedResponse)
changesItemByAccount[accountId] = changesItemMapper(oldState, newState, hasMoreChanges, created, updated, destroyed)
stateByAccountId[accountId] = stateMapper(createdResponse)
}
return respMapper(changesItemByAccount), squashState(stateByAccountId), nil
})
}

View File

@@ -14,6 +14,7 @@ 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 {
@@ -72,7 +73,7 @@ func command[T any](api ApiClient, //NOSONAR
if err != nil {
logger.Error().Err(err).Msgf("failed to deserialize body JSON payload into a %T", response)
var zero T
return zero, "", "", language, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
return zero, "", "", language, jmapError(err, JmapErrorDecodingResponseBody)
}
if response.SessionState != session.State {
@@ -96,6 +97,9 @@ func command[T any](api ApiClient, //NOSONAR
code = JmapErrorUnknownMethod
case MethodLevelErrorInvalidArguments:
code = JmapErrorInvalidArguments
if strings.HasPrefix(errorParameters.Description, "invalid JMAP State") {
code = JmapInvalidObjectState
}
case MethodLevelErrorInvalidResultReference:
code = JmapErrorInvalidResultReference
case MethodLevelErrorForbidden:
@@ -119,14 +123,14 @@ func command[T any](api ApiClient, //NOSONAR
err = errors.New(msg)
logger.Warn().Int("code", code).Str("type", errorParameters.Type).Msg(msg)
var zero T
return zero, response.SessionState, "", language, SimpleError{code: code, err: err}
return zero, response.SessionState, "", language, jmapResponseError(code, err, errorParameters.Type, errorParameters.Description)
} else {
code := JmapErrorUnspecifiedType
msg := fmt.Sprintf("found method level error in response '%v'", mr.Tag)
err := errors.New(msg)
logger.Warn().Int("code", code).Msg(msg)
var zero T
return zero, response.SessionState, "", language, SimpleError{code: code, err: err}
return zero, response.SessionState, "", language, jmapResponseError(code, err, errorParameters.Type, errorParameters.Description)
}
}
}
@@ -200,19 +204,35 @@ func retrieveResponseMatchParameters[T any](logger *log.Logger, data *Response,
if !ok {
err := fmt.Errorf("failed to find JMAP response invocation match for command '%v' and tag '%v'", command, tag)
logger.Error().Msg(err.Error())
return simpleError(err, JmapErrorInvalidJmapResponsePayload)
return jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
params := match.Parameters
typedParams, ok := params.(T)
if !ok {
err := fmt.Errorf("JMAP response invocation matches command '%v' and tag '%v' but the type %T does not match the expected %T", command, tag, params, *target)
logger.Error().Msg(err.Error())
return simpleError(err, JmapErrorInvalidJmapResponsePayload)
return jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
*target = typedParams
return nil
}
func tryRetrieveResponseMatchParameters[T any](logger *log.Logger, data *Response, command Command, tag string, target *T) (bool, Error) {
match, ok := retrieveResponseMatch(data, command, tag)
if !ok {
return false, nil
}
params := match.Parameters
typedParams, ok := params.(T)
if !ok {
err := fmt.Errorf("JMAP response invocation matches command '%v' and tag '%v' but the type %T does not match the expected %T", command, tag, params, *target)
logger.Error().Msg(err.Error())
return true, jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
*target = typedParams
return true, nil
}
func (i *Invocation) MarshalJSON() ([]byte, error) {
// JMAP requests have a slightly unusual structure since they are not a JSON object
// but, instead, a three-element array composed of
@@ -268,6 +288,14 @@ 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)
}
func squashStateFunc[V any](all map[string]V, mapper func(V) State) State {
n := len(all)
if n == 0 {
@@ -346,3 +374,11 @@ func boolPtr(b bool) *bool {
func identity1[T any](t T) T {
return t
}
func posUIntPtr(i uint) *uint {
if i > 0 {
return &i
} else {
return nil
}
}

View File

@@ -22,3 +22,14 @@ func TestUnmarshallingError(t *testing.T) {
require.Equal("forbidden", er.Type)
require.Equal("You do not have access to account a", er.Description)
}
func TestSquashKeyedStates(t *testing.T) {
require := require.New(t)
result := squashKeyedStates(map[string]State{
"a": "aaa",
"b": "bbb",
"c": "ccc",
})
require.Equal("a:aaa,b:bbb,c:ccc", string(result))
}

View File

@@ -51,7 +51,7 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt
list := make([]AccountWithIdAndIdentities, len(req.session.Accounts))
i := 0
for accountId, account := range req.session.Accounts {
identities, ok := resp.Identities[accountId]
identities, ok := resp[accountId]
if !ok {
identities = []jmap.Identity{}
}

View File

@@ -55,6 +55,41 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
})
}
// Get the changes that occured in a given mailbox since a certain state.
// @api:tags calendars,changes
func (g *Groupware) GetCalendarChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list calendar changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetCalendarChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, CalendarResponseObjectType, state)
})
}
// Get all the events in a calendar of an account by its identifier.
func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { //NOSONAR
g.respond(w, r, func(req Request) Response {
@@ -106,6 +141,39 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
})
}
// Get changes to Contacts since a given State
// @api:tags event,changes
func (g *Groupware) GetEventChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
var maxChanges uint = 0
if v, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0); err != nil {
return req.error(accountId, err)
} else if ok {
maxChanges = v
l = l.Uint(QueryParamMaxChanges, v)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list event changes"))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetCalendarEventChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.CalendarEventChanges = changes
return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
})
}
func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()

View File

@@ -0,0 +1,78 @@
package groupware
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Retrieve changes for multiple or all Groupware objects, based on their respective state token.
//
// Since each object type has its own state token, the request must include the token for each
// object type separately.
//
// This is done through individual query parameter, as follows:
// `?emails=rafrag&contacts=rbsxqeay&events=n`
//
// Additionally, the `maxchanges` query parameter may be used to limit the number of changes
// to retrieve for each object type -- this is applied to each object type, not overall.
//
// If `maxchanges` is not specifed or if `maxchanges` has the value `0`, then there is no limit
// and all the changes from the specified state to now are included in the result.
//
// The response then includes the new state after that maximum number if changes,
// as well as a `hasMoreChanges` boolean flag which can be used to paginate the retrieval of
// changes and the objects associated with the identifiers.
func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSONAR
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
var maxChanges uint = 0
if v, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0); err != nil {
return req.error(accountId, err)
} else if ok {
maxChanges = v
l = l.Uint(QueryParamMaxChanges, v)
}
sinceState := jmap.StateMap{}
{
if state, ok := req.getStringParam(QueryParamMailboxes, ""); ok {
sinceState.Mailboxes = ptr(toState(state))
}
if state, ok := req.getStringParam(QueryParamEmails, ""); ok {
sinceState.Emails = ptr(toState(state))
}
if state, ok := req.getStringParam(QueryParamAddressbooks, ""); ok {
sinceState.Addressbooks = ptr(toState(state))
}
if state, ok := req.getStringParam(QueryParamContacts, ""); ok {
sinceState.Contacts = ptr(toState(state))
}
if state, ok := req.getStringParam(QueryParamCalendars, ""); ok {
sinceState.Calendars = ptr(toState(state))
}
if state, ok := req.getStringParam(QueryParamEvents, ""); ok {
sinceState.Events = ptr(toState(state))
}
if sinceState.IsZero() {
return req.noop(accountId)
}
}
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.Changes = changes
return req.respond(accountId, body, sessionState, "", state)
})
}

View File

@@ -89,6 +89,41 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
})
}
// Get the changes that occured in a given mailbox since a certain state.
// @api:tags mailbox,changes
func (g *Groupware) GetAddressBookChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list addressbook changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetAddressbookChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, AddressBookResponseObjectType, state)
})
}
// Get all the contacts in an addressbook of an account by its identifier.
func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { //NOSONAR
g.respond(w, r, func(req Request) Response {
@@ -214,14 +249,11 @@ func (g *Groupware) GetContactsChanges(w http.ResponseWriter, r *http.Request) {
l = l.Uint(QueryParamMaxChanges, v)
}
sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes")
if err != nil {
return req.error(accountId, err)
}
l = l.Str(HeaderParamSince, log.SafeString(sinceState))
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list contact changes"))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetContactCardsSince(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
changes, sessionState, state, lang, jerr := g.jmap.GetContactCardChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
// Get a specific mailbox by its identifier.
@@ -62,7 +63,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS
hasCriteria = true
}
role, ok := req.getStringParam(QueryParamMailboxSearchRole, "") // the mailbox role to filter on
if role != "" {
if ok && role != "" {
filter.Role = role
hasCriteria = true
}
@@ -195,9 +196,9 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list mailbox changes")
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list mailbox changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(sinceState))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
@@ -232,13 +233,13 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
allAccountIds := req.AllAccountIds()
l.Array(logAccountId, log.SafeStringArray(allAccountIds))
sinceStateMap, ok, err := req.parseMapParam(QueryParamSince)
sinceStateStrMap, ok, err := req.parseMapParam(QueryParamSince)
if err != nil {
return req.errorN(allAccountIds, err)
}
if ok {
dict := zerolog.Dict()
for k, v := range sinceStateMap {
for k, v := range sinceStateStrMap {
dict.Str(log.SafeString(k), log.SafeString(v))
}
l = l.Dict(QueryParamSince, dict)
@@ -254,6 +255,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
logger := log.From(l)
sinceStateMap := structs.MapValues(sinceStateStrMap, toState)
changesByAccountId, sessionState, state, lang, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language(), sinceStateMap, maxChanges)
if jerr != nil {
return req.jmapErrorN(allAccountIds, jerr, sessionState, lang)

View File

@@ -0,0 +1,106 @@
package groupware
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
type ObjectsRequest struct {
Mailboxes []string `json:"mailboxes,omitempty"`
Emails []string `json:"emails,omitempty"`
Addressbooks []string `json:"addressbooks,omitempty"`
Contacts []string `json:"contacts,omitempty"`
Calendars []string `json:"calendars,omitempty"`
Events []string `json:"events,omitempty"`
}
// Retrieve changes for multiple or all Groupware objects, based on their respective state token.
//
// Since each object type has its own state token, the request must include the token for each
// object type separately.
//
// This is done through individual query parameter, as follows:
// `?emails=rafrag&contacts=rbsxqeay&events=n`
//
// Additionally, the `maxchanges` query parameter may be used to limit the number of changes
// to retrieve for each object type -- this is applied to each object type, not overall.
//
// If `maxchanges` is not specifed or if `maxchanges` has the value `0`, then there is no limit
// and all the changes from the specified state to now are included in the result.
//
// The response then includes the new state after that maximum number if changes,
// as well as a `hasMoreChanges` boolean flag which can be used to paginate the retrieval of
// changes and the objects associated with the identifiers.
func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSONAR
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
mailboxIds := []string{}
emailIds := []string{}
addressbookIds := []string{}
contactIds := []string{}
calendarIds := []string{}
eventIds := []string{}
{
var objects ObjectsRequest
if ok, err := req.optBody(&objects); err != nil {
return req.error(accountId, err)
} else if ok {
mailboxIds = append(mailboxIds, objects.Mailboxes...)
emailIds = append(mailboxIds, objects.Emails...)
addressbookIds = append(mailboxIds, objects.Addressbooks...)
contactIds = append(mailboxIds, objects.Contacts...)
calendarIds = append(mailboxIds, objects.Calendars...)
eventIds = append(mailboxIds, objects.Events...)
}
}
if list, ok, err := req.parseOptStringListParam(QueryParamMailboxes); err != nil {
return req.error(accountId, err)
} else if ok {
mailboxIds = append(mailboxIds, list...)
}
if list, ok, err := req.parseOptStringListParam(QueryParamEmails); err != nil {
return req.error(accountId, err)
} else if ok {
emailIds = append(emailIds, list...)
}
if list, ok, err := req.parseOptStringListParam(QueryParamAddressbooks); err != nil {
return req.error(accountId, err)
} else if ok {
addressbookIds = append(addressbookIds, list...)
}
if list, ok, err := req.parseOptStringListParam(QueryParamContacts); err != nil {
return req.error(accountId, err)
} else if ok {
contactIds = append(contactIds, list...)
}
if list, ok, err := req.parseOptStringListParam(QueryParamCalendars); err != nil {
return req.error(accountId, err)
} else if ok {
calendarIds = append(calendarIds, list...)
}
if list, ok, err := req.parseOptStringListParam(QueryParamEvents); err != nil {
return req.error(accountId, err)
} else if ok {
eventIds = append(eventIds, list...)
}
logger := log.From(l)
objs, sessionState, state, lang, jerr := g.jmap.GetObjects(accountId, req.session, req.ctx, logger, req.language(),
mailboxIds, emailIds, addressbookIds, contactIds, calendarIds, eventIds)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.Objects = objs
return req.respond(accountId, body, sessionState, "", state)
})
}

View File

@@ -138,6 +138,8 @@ func groupwareErrorFromJmap(j jmap.Error) *GroupwareError {
return &ErrorInvalidRequestPayload
case jmap.JmapErrorInvalidJmapResponsePayload:
return &ErrorInvalidResponsePayload
case jmap.JmapInvalidObjectState:
return &ErrorInvalidObjectState
case jmap.JmapErrorUnspecifiedType, jmap.JmapErrorUnknownMethod, jmap.JmapErrorInvalidArguments, jmap.JmapErrorInvalidResultReference:
return &ErrorInvalidGroupwareRequest
case jmap.JmapErrorServerUnavailable:
@@ -203,6 +205,7 @@ const (
ErrorCodeNoMailboxWithSentRole = "NMBXSE"
ErrorCodeInvalidSortSpecification = "INVSSP"
ErrorCodeInvalidSortProperty = "INVSPR"
ErrorCodeInvalidObjectState = "INVOST"
)
var (
@@ -470,6 +473,12 @@ var (
Title: "Invalid sort property",
Detail: "The sort property in the query parameter does not exist or is not acceptable.",
}
ErrorInvalidObjectState = GroupwareError{
Status: http.StatusBadRequest,
Code: ErrorCodeInvalidObjectState,
Title: "Invalid Object State",
Detail: "The request included an object state that does not exist.",
}
)
type ErrorOpt interface {

View File

@@ -233,7 +233,7 @@ func (r *Request) getStringParam(param string, defaultValue string) (string, boo
}
str := q.Get(param)
if str == "" {
return defaultValue, false
return defaultValue, true
}
return str, true
}
@@ -346,23 +346,23 @@ func (r *Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *
return b, true, nil
}
// Parses query parameters that have the form ?param.field1=...&param.field2=... into a map of strings.
// When multiple values are defined for a field, the last one wins.
func (r *Request) parseMapParam(param string) (map[string]string, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return map[string]string{}, false, nil
}
result := map[string]string{}
prefix := param + "."
found := false
for name, values := range q {
if strings.HasPrefix(name, prefix) {
found = true
if len(values) > 0 {
key := name[len(prefix)+1:]
result[key] = values[0]
key := name[len(prefix):]
result[key] = values[len(values)-1]
}
}
}
return result, true, nil
return result, found, nil
}
func (r *Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
@@ -402,6 +402,26 @@ func (r *Request) body(target any) *Error {
return nil
}
func (r *Request) optBody(target any) (bool, *Error) {
body := r.r.Body
defer func(b io.ReadCloser) {
err := b.Close()
if err != nil {
r.logger.Error().Err(err).Msg("failed to close request body")
}
}(body)
if body == nil || body == http.NoBody { // not sure whether this is always enough to detect an empty body, we might have to read the whole body into memory first
return false, nil
}
err := json.NewDecoder(body).Decode(target)
if err != nil {
r.logger.Warn().Msgf("failed to deserialize the request body: %s", err.Error())
return true, r.observedParameterError(ErrorInvalidRequestBody, withSource(&ErrorSource{Pointer: "/"})) // we don't get any details here
}
return true, nil
}
func (r *Request) language() string {
return r.r.Header.Get("Accept-Language")
}
@@ -573,3 +593,11 @@ func mapSort[T any](accountIds []string, req *Request, defaultSort []T, props []
return defaultSort, true, Response{}
}
}
func toState(s string) jmap.State {
return jmap.State(s)
}
func ptr[T any](t T) *T {
return &t
}

View File

@@ -2,7 +2,9 @@ package groupware
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/require"
@@ -62,3 +64,37 @@ func TestParseSort(t *testing.T) {
require.False(res[1].Ascending)
}
}
func TestParseMap(t *testing.T) {
require := require.New(t)
for _, tt := range []struct {
uri string
ok bool
out map[string]string
}{
{"/foo", false, map[string]string{}},
{"/foo?name=camina", false, map[string]string{}},
{"/foo?map=camina", false, map[string]string{}},
{"/foo?map.name=camina", true, map[string]string{"name": "camina"}},
{"/foo?map.gn=camina&map.sn=drummer", true, map[string]string{"gn": "camina", "sn": "drummer"}},
{"/foo?map.name=camina&map.name=chrissie", true, map[string]string{"name": "chrissie"}},
} {
t.Run(fmt.Sprintf("uri:%s", tt.uri), func(t *testing.T) {
var req Request
{
u, err := url.Parse(tt.uri)
require.NoError(err)
req = Request{r: &http.Request{URL: u}, ctx: context.Background()}
}
res, ok, err := req.parseMapParam("map")
require.Nil(err)
if tt.ok {
require.True(ok)
require.Equal(res, tt.out)
} else {
require.False(ok)
require.Empty(res)
}
})
}
}

View File

@@ -58,6 +58,12 @@ const (
QueryParamUndesirable = "undesirable"
QueryParamMarkAsSeen = "markAsSeen"
QueryParamSort = "sort"
QueryParamMailboxes = "mailboxes"
QueryParamEmails = "emails"
QueryParamAddressbooks = "addressbooks"
QueryParamContacts = "contacts"
QueryParamCalendars = "calendars"
QueryParamEvents = "events"
HeaderParamSince = "if-none-match"
)
@@ -154,7 +160,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetCalendars)
r.Route("/{calendarid}", func(r chi.Router) {
r.Get("/", g.GetCalendarById)
r.Get("/events", g.GetEventsInCalendar)
r.Get("/events", g.GetEventsInCalendar) //NOSONAR
})
})
r.Route("/events", func(r chi.Router) {
@@ -169,10 +175,16 @@ func (g *Groupware) Route(r chi.Router) {
})
})
r.Route("/changes", func(r chi.Router) {
r.Get("/contacts", g.GetContactsChanges)
r.Get("/", g.GetChanges)
r.Get("/mailboxes", g.GetMailboxChanges)
r.Get("/emails", g.GetEmailChanges)
r.Get("/addressbooks", g.GetAddressBookChanges)
r.Get("/contacts", g.GetContactsChanges)
r.Get("/calendars", g.GetCalendarChanges)
r.Get("/events", g.GetEventChanges)
})
r.Get("/objects", g.GetObjects)
r.Post("/objects", g.GetObjects)
})
})

View File

@@ -175,6 +175,7 @@ objectClass: groupOfNames
objectClass: openCloudObject
objectClass: top
cn: programmers
mail: programmers@example.org
description: Computer Programmers
openCloudUUID: ce4aa240-dd94-11ef-82b8-4f4828849072
member: uid=alan,ou=users,o=libregraph-idm