mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-13 11:57:33 -04:00
groupware: framework refactorings + add support for /changes + add /objects
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
@@ -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
144
pkg/jmap/api_changes.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
118
pkg/jmap/api_objects.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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{},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; 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 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{} },
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
78
services/groupware/pkg/groupware/api_changes.go
Normal file
78
services/groupware/pkg/groupware/api_changes.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
106
services/groupware/pkg/groupware/api_objects.go
Normal file
106
services/groupware/pkg/groupware/api_objects.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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=...¶m.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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user