From 98e1701c0a569b61c2fbb7e5f8d2c9a9db78a3af Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Fri, 27 Mar 2026 15:54:25 +0100 Subject: [PATCH] groupware: add changes support for quotas, identities, submissions --- pkg/jmap/api_blob.go | 4 +- pkg/jmap/api_bootstrap.go | 4 +- pkg/jmap/api_calendar.go | 16 +- pkg/jmap/api_changes.go | 83 +++- pkg/jmap/api_contact.go | 18 +- pkg/jmap/api_email.go | 78 +++- pkg/jmap/api_identity.go | 61 ++- pkg/jmap/api_mailbox.go | 109 +---- pkg/jmap/api_objects.go | 72 +++- pkg/jmap/api_quota.go | 88 +++- pkg/jmap/api_vacation.go | 6 +- pkg/jmap/client.go | 12 +- pkg/jmap/integration_test.go | 8 +- pkg/jmap/model.go | 406 +++++++++++++----- pkg/jmap/templates.go | 77 +++- pkg/jmap/tools.go | 9 + .../groupware/pkg/groupware/api_changes.go | 7 + .../groupware/pkg/groupware/api_objects.go | 48 ++- services/groupware/pkg/groupware/api_quota.go | 33 ++ services/groupware/pkg/groupware/route.go | 3 + 20 files changed, 835 insertions(+), 307 deletions(-) diff --git a/pkg/jmap/api_blob.go b/pkg/jmap/api_blob.go index c44a5bf904..40b6402f07 100644 --- a/pkg/jmap/api_blob.go +++ b/pkg/jmap/api_blob.go @@ -10,7 +10,7 @@ import ( ) func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (*Blob, SessionState, State, Language, Error) { - cmd, jerr := j.request(session, logger, + cmd, jerr := j.request(session, logger, ns(JmapBlob), invocation(CommandBlobGet, BlobGetCommand{ AccountId: accountId, Ids: []string{id}, @@ -89,7 +89,7 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont Properties: []string{BlobPropertyDigestSha512}, } - cmd, jerr := j.request(session, logger, + cmd, jerr := j.request(session, logger, ns(JmapBlob), invocation(CommandBlobUpload, upload, "0"), invocation(CommandBlobGet, getHash, "1"), ) diff --git a/pkg/jmap/api_bootstrap.go b/pkg/jmap/api_bootstrap.go index 532a8a4001..31c44e6e42 100644 --- a/pkg/jmap/api_bootstrap.go +++ b/pkg/jmap/api_bootstrap.go @@ -12,6 +12,8 @@ type AccountBootstrapResult struct { Quotas []Quota `json:"quotas,omitempty"` } +var NS_MAIL_QUOTA = ns(JmapMail, JmapQuota) + func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AccountBootstrapResult, SessionState, State, Language, Error) { //NOSONAR uniqueAccountIds := structs.Uniq(accountIds) @@ -23,7 +25,7 @@ func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context calls[i*2+1] = invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId}, mcid(accountId, "Q")) } - cmd, err := j.request(session, logger, calls...) + cmd, err := j.request(session, logger, NS_MAIL_QUOTA, calls...) if err != nil { return nil, "", "", "", err } diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 3de6d55925..47d895f34c 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -7,10 +7,12 @@ import ( "github.com/opencloud-eu/opencloud/pkg/structs" ) +var NS_CALENDARS = ns(JmapCalendars) + func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, blobIds []string) (CalendarEventParseResponse, SessionState, State, Language, Error) { logger = j.logger("ParseICalendarBlob", session, logger) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_CALENDARS, invocation(CommandCalendarEventParse, CalendarEventParseCommand{AccountId: accountId, BlobIds: blobIds}, "0"), ) if err != nil { @@ -33,7 +35,7 @@ type CalendarsResponse struct { } func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarsResponse, SessionState, State, Language, Error) { - return getTemplate(j, "GetCalendars", CommandCalendarGet, + return getTemplate(j, "GetCalendars", NS_CALENDARS, CommandCalendarGet, func(accountId string, ids []string) CalendarGetCommand { return CalendarGetCommand{AccountId: accountId, Ids: ids} }, @@ -57,7 +59,7 @@ type CalendarChanges struct { // 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", + return changesTemplate(j, "GetCalendarChanges", NS_CALENDARS, CommandCalendarChanges, CommandCalendarGet, func() CalendarChangesCommand { return CalendarChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} @@ -126,7 +128,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx // Properties: CalendarEventProperties, // to also retrieve UTCStart and UTCEnd }, mcid(accountId, "1")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_CALENDARS, invocations...) if err != nil { return nil, "", "", "", err } @@ -161,7 +163,7 @@ type CalendarEventChanges struct { 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", + return changesTemplate(j, "GetCalendarEventChanges", NS_CALENDARS, CommandCalendarEventChanges, CommandCalendarEventGet, func() CalendarEventChangesCommand { return CalendarEventChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} @@ -196,7 +198,7 @@ func (j *Client) GetCalendarEventChanges(accountId string, session *Session, ctx } 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, + return createTemplate(j, "CreateCalendarEvent", NS_CALENDARS, CalendarEventType, CommandCalendarEventSet, CommandCalendarEventGet, func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand { return CalendarEventSetCommand{AccountId: accountId, Create: create} }, @@ -219,7 +221,7 @@ func (j *Client) CreateCalendarEvent(accountId string, session *Session, ctx con } func (j *Client) DeleteCalendarEvent(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { - return deleteTemplate(j, "DeleteCalendarEvent", CommandCalendarEventSet, + return deleteTemplate(j, "DeleteCalendarEvent", NS_CALENDARS, CommandCalendarEventSet, func(accountId string, destroy []string) CalendarEventSetCommand { return CalendarEventSetCommand{AccountId: accountId, Destroy: destroy} }, diff --git a/pkg/jmap/api_changes.go b/pkg/jmap/api_changes.go index da319b2ede..7c763e7216 100644 --- a/pkg/jmap/api_changes.go +++ b/pkg/jmap/api_changes.go @@ -7,30 +7,43 @@ import ( "github.com/rs/zerolog" ) +// Note that Quota/changes is currently not supported in Stalwart, as it always gives a +// cannotCalculateChanges error back. + +var NS_CHANGES = ns(JmapMail, JmapContacts, JmapCalendars) //, JmapQuota) + 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"` + 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"` + Identities *IdentityChangesResponse `json:"identities,omitempty"` + EmailSubmissions *EmailSubmissionChangesResponse `json:"submissions,omitempty"` + // Quotas *QuotaChangesResponse `json:"quotas,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"` + 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"` + Identities *State `json:"identities,omitempty"` + EmailSubmissions *State `json:"submissions,omitempty"` + // Quotas *State `json:"quotas,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 + s.Events == nil && s.Addressbooks == nil && s.Contacts == nil && + s.Identities == nil && s.EmailSubmissions == nil + //s.Quotas == nil } func (s StateMap) MarshalZerologObject(e *zerolog.Event) { @@ -52,6 +65,13 @@ func (s StateMap) MarshalZerologObject(e *zerolog.Event) { if s.Contacts != nil { e.Str("contacts", string(*s.Contacts)) } + if s.Identities != nil { + e.Str("identities", string(*s.Identities)) + } + if s.EmailSubmissions != nil { + e.Str("submissions", string(*s.EmailSubmissions)) + } + // if s.Quotas != nil { e.Str("quotas", string(*s.Quotas)) } } 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 @@ -79,8 +99,15 @@ func (j *Client) GetChanges(accountId string, session *Session, ctx context.Cont if stateMap.Contacts != nil { methodCalls = append(methodCalls, invocation(CommandContactCardChanges, ContactCardChangesCommand{AccountId: accountId, SinceState: *stateMap.Contacts, MaxChanges: posUIntPtr(maxChanges)}, "contacts")) } + if stateMap.Identities != nil { + methodCalls = append(methodCalls, invocation(CommandIdentityChanges, IdentityChangesCommand{AccountId: accountId, SinceState: *stateMap.Identities, MaxChanges: posUIntPtr(maxChanges)}, "identities")) + } + if stateMap.EmailSubmissions != nil { + methodCalls = append(methodCalls, invocation(CommandEmailSubmissionChanges, EmailSubmissionChangesCommand{AccountId: accountId, SinceState: *stateMap.EmailSubmissions, MaxChanges: posUIntPtr(maxChanges)}, "submissions")) + } + // if stateMap.Quotas != nil { methodCalls = append(methodCalls, invocation(CommandQuotaChanges, QuotaChangesCommand{AccountId: accountId, SinceState: *stateMap.Quotas, MaxChanges: posUIntPtr(maxChanges)}, "quotas")) } - cmd, err := j.request(session, logger, methodCalls...) + cmd, err := j.request(session, logger, NS_CHANGES, methodCalls...) if err != nil { return Changes{}, "", "", "", err } @@ -139,6 +166,32 @@ func (j *Client) GetChanges(accountId string, session *Session, ctx context.Cont states["contacts"] = contacts.NewState } + var identities IdentityChangesResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandIdentityChanges, "identities", &identities); err != nil { + return Changes{}, "", err + } else if ok { + changes.Identities = &identities + states["identities"] = identities.NewState + } + + var submissions EmailSubmissionChangesResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandEmailSubmissionChanges, "submissions", &submissions); err != nil { + return Changes{}, "", err + } else if ok { + changes.EmailSubmissions = &submissions + states["submissions"] = submissions.NewState + } + + /* + var quotas QuotaChangesResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandQuotaChanges, "quotas", "as); err != nil { + return Changes{}, "", err + } else if ok { + changes.Quotas = "as + states["quotas"] = quotas.NewState + } + */ + return changes, squashKeyedStates(states), nil }) } diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 92ddd47fa1..0095a9c171 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -9,6 +9,8 @@ import ( "github.com/opencloud-eu/opencloud/pkg/structs" ) +var NS_CONTACTS = ns(JmapContacts) + type AddressBooksResponse struct { AddressBooks []AddressBook `json:"addressbooks"` NotFound []string `json:"notFound,omitempty"` @@ -17,7 +19,7 @@ type AddressBooksResponse struct { func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, State, Language, Error) { logger = j.logger("GetAddressbooks", session, logger) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_CONTACTS, invocation(CommandAddressBookGet, AddressBookGetCommand{AccountId: accountId, Ids: ids}, "0"), ) if err != nil { @@ -49,7 +51,7 @@ type AddressBookChanges struct { // 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", + return changesTemplate(j, "GetAddressbookChanges", NS_CONTACTS, CommandAddressBookChanges, CommandAddressBookGet, func() AddressBookChangesCommand { return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} @@ -87,7 +89,7 @@ func (j *Client) GetContactCardsById(accountId string, session *Session, ctx con acceptLanguage string, contactIds []string) (map[string]jscontact.ContactCard, SessionState, State, Language, Error) { logger = j.logger("GetContactCardsById", session, logger) - cmd, err := j.request(session, logger, invocation(CommandContactCardGet, ContactCardGetCommand{ + cmd, err := j.request(session, logger, NS_CONTACTS, invocation(CommandContactCardGet, ContactCardGetCommand{ Ids: contactIds, AccountId: accountId, }, "0")) @@ -111,7 +113,7 @@ 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) { - return getTemplate(j, "GetContactCards", CommandContactCardGet, + return getTemplate(j, "GetContactCards", NS_CONTACTS, CommandContactCardGet, func(accountId string, ids []string) ContactCardGetCommand { return ContactCardGetCommand{AccountId: accountId, Ids: contactIds} }, @@ -132,7 +134,7 @@ type ContactCardChanges struct { 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", + return changesTemplate(j, "GetContactCardChanges", NS_CONTACTS, CommandContactCardChanges, CommandContactCardGet, func() ContactCardChangesCommand { return ContactCardChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} @@ -200,7 +202,7 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co }, }, mcid(accountId, "1")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_CONTACTS, invocations...) if err != nil { return nil, "", "", "", err } @@ -227,7 +229,7 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (*jscontact.ContactCard, SessionState, State, Language, Error) { logger = j.logger("CreateContactCard", session, logger) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_CONTACTS, invocation(CommandContactCardSet, ContactCardSetCommand{ AccountId: accountId, Create: map[string]jscontact.ContactCard{ @@ -281,7 +283,7 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { logger = j.logger("DeleteContactCard", session, logger) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_CONTACTS, invocation(CommandContactCardSet, ContactCardSetCommand{ AccountId: accountId, Destroy: destroy, diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index d421fd77bf..bc8b2a960a 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -11,6 +11,9 @@ import ( "github.com/rs/zerolog" ) +var NS_MAIL = ns(JmapMail) +var NS_MAIL_SUBMISSION = ns(JmapMail, JmapSubmission) + type Emails struct { Emails []Email `json:"emails,omitempty"` Total uint `json:"total,omitzero"` @@ -54,7 +57,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte methodCalls = append(methodCalls, invocation(CommandThreadGet, threads, "2")) } - cmd, err := j.request(session, logger, methodCalls...) + cmd, err := j.request(session, logger, NS_MAIL, methodCalls...) if err != nil { return nil, nil, "", "", "", err } @@ -92,7 +95,7 @@ func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context. logger = j.logger("GetEmailBlobId", session, logger) get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}} - cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0")) + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandEmailGet, get, "0")) if err != nil { return "", "", "", "", err } @@ -156,7 +159,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c invocations = append(invocations, invocation(CommandThreadGet, threads, "2")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAIL, invocations...) if err != nil { return Emails{}, "", "", "", err } @@ -232,7 +235,7 @@ func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context getUpdated.MaxBodyValueBytes = maxBodyValueBytes } - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandEmailChanges, changes, "0"), invocation(CommandEmailGet, getCreated, "1"), invocation(CommandEmailGet, getUpdated, "2"), @@ -336,7 +339,7 @@ func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterEleme invocations[i*3+2] = invocation(CommandSearchSnippetGet, snippet, mcid(accountId, "2")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAIL, invocations...) if err != nil { return nil, "", "", "", err } @@ -440,7 +443,7 @@ func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, ses invocations[i*2+1] = invocation(CommandEmailGet, mails, mcid(accountId, "1")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAIL, invocations...) if err != nil { return nil, "", "", "", err } @@ -532,7 +535,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilter invocations[i*3+2] = invocation(CommandEmailGet, mails, mcid(accountId, "2")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAIL, invocations...) if err != nil { return nil, "", "", "", err } @@ -623,7 +626,7 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con Properties: []string{BlobPropertyDigestSha512}, } - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandBlobUpload, upload, "0"), invocation(CommandBlobGet, getHash, "1"), ) @@ -682,7 +685,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri set.Destroy = []string{replaceId} } - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandEmailSet, set, "0"), ) if err != nil { @@ -727,7 +730,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri // // To delete mails, use the DeleteEmails function instead. func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]*Email, SessionState, State, Language, Error) { - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandEmailSet, EmailSetCommand{ AccountId: accountId, Update: updates, @@ -754,7 +757,7 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, } func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandEmailSet, EmailSetCommand{ AccountId: accountId, Destroy: destroy, @@ -836,7 +839,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string Ids: []string{"#" + id}, } - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL_SUBMISSION, invocation(CommandEmailSubmissionSet, set, "0"), invocation(CommandEmailSubmissionGet, get, "1"), ) @@ -897,7 +900,7 @@ type emailSubmissionResult struct { func (j *Client) GetEmailSubmissionStatus(accountId string, submissionIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]EmailSubmission, []string, SessionState, State, Language, Error) { logger = j.logger("GetEmailSubmissionStatus", session, logger) - cmd, err := j.request(session, logger, invocation(CommandEmailSubmissionGet, EmailSubmissionGetCommand{ + cmd, err := j.request(session, logger, NS_MAIL_SUBMISSION, invocation(CommandEmailSubmissionGet, EmailSubmissionGetCommand{ AccountId: accountId, Ids: submissionIds, }, "0")) @@ -926,7 +929,7 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId)) }) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_MAIL, invocation(CommandThreadGet, ThreadGetCommand{ AccountId: accountId, Ids: []string{threadId}, @@ -1025,7 +1028,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx }, mcid(accountId, "2")) } } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAIL, invocations...) if err != nil { return nil, "", "", "", err } @@ -1068,6 +1071,51 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx }) } +type EmailSubmissionChanges struct { + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + HasMoreChanges bool `json:"hasMoreChanges"` + Created []EmailSubmission `json:"created,omitempty"` + Updated []EmailSubmission `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` +} + +func (j *Client) GetEmailSubmissionChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, sinceState State, maxChanges uint) (EmailSubmissionChanges, SessionState, State, Language, Error) { + return changesTemplate(j, "GetEmailSubmissionChanges", NS_MAIL_SUBMISSION, + CommandEmailSubmissionChanges, CommandEmailSubmissionGet, + func() EmailSubmissionChangesCommand { + return EmailSubmissionChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} + }, + func(path string, rof string) EmailSubmissionGetRefCommand { + return EmailSubmissionGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandEmailSubmissionChanges, + Path: path, + ResultOf: rof, + }, + } + }, + func(resp EmailSubmissionChangesResponse) (State, State, bool, []string) { + return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed + }, + func(resp EmailSubmissionGetResponse) []EmailSubmission { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, created, updated []EmailSubmission, destroyed []string) EmailSubmissionChanges { + return EmailSubmissionChanges{ + OldState: oldState, + NewState: newState, + HasMoreChanges: hasMoreChanges, + Created: created, + Updated: updated, + Destroyed: destroyed, + } + }, + func(resp EmailSubmissionGetResponse) State { return resp.State }, + session, ctx, logger, acceptLanguage, + ) +} + func setThreadSize(threads *ThreadGetResponse, emails []Email) { threadSizeById := make(map[string]int, len(threads.List)) for _, thread := range threads.List { diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index 65cf3ef8f1..3387daa1ce 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -8,8 +8,10 @@ import ( "github.com/opencloud-eu/opencloud/pkg/structs" ) +var NS_IDENTITY = ns(JmapMail) + func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]Identity, SessionState, State, Language, Error) { - return getTemplate(j, "GetAllIdentities", CommandIdentityGet, + return getTemplate(j, "GetAllIdentities", NS_IDENTITY, CommandIdentityGet, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId} }, @@ -20,7 +22,7 @@ func (j *Client) GetAllIdentities(accountId string, session *Session, ctx contex } func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) ([]Identity, SessionState, State, Language, Error) { - return getTemplate(j, "GetIdentities", CommandIdentityGet, + return getTemplate(j, "GetIdentities", NS_IDENTITY, CommandIdentityGet, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId, Ids: ids} }, @@ -31,7 +33,7 @@ func (j *Client) GetIdentities(accountId string, session *Session, ctx context.C } 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, + return getTemplateN(j, "GetIdentitiesForAllAccounts", NS_IDENTITY, CommandIdentityGet, func(accountId string, ids []string) IdentityGetCommand { return IdentityGetCommand{AccountId: accountId} }, @@ -59,7 +61,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [ calls[i+1] = invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, strconv.Itoa(i+1)) } - cmd, err := j.request(session, logger, calls...) + cmd, err := j.request(session, logger, NS_IDENTITY, calls...) if err != nil { return IdentitiesAndMailboxesGetResponse{}, "", "", "", err } @@ -95,7 +97,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [ func (j *Client) CreateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (Identity, SessionState, State, Language, Error) { logger = j.logger("CreateIdentity", session, logger) - cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{ + cmd, err := j.request(session, logger, NS_IDENTITY, invocation(CommandIdentitySet, IdentitySetCommand{ AccountId: accountId, Create: map[string]Identity{ "c": identity, @@ -121,7 +123,7 @@ func (j *Client) CreateIdentity(accountId string, session *Session, ctx context. func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (Identity, SessionState, State, Language, Error) { logger = j.logger("UpdateIdentity", session, logger) - cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{ + cmd, err := j.request(session, logger, NS_IDENTITY, invocation(CommandIdentitySet, IdentitySetCommand{ AccountId: accountId, Update: map[string]PatchObject{ "c": identity.AsPatch(), @@ -147,7 +149,7 @@ func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context. func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) ([]string, SessionState, State, Language, Error) { logger = j.logger("DeleteIdentity", session, logger) - cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{ + cmd, err := j.request(session, logger, NS_IDENTITY, invocation(CommandIdentitySet, IdentitySetCommand{ AccountId: accountId, Destroy: ids, }, "0")) @@ -168,3 +170,48 @@ func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context. return response.Destroyed, response.NewState, nil }) } + +type IdentityChanges struct { + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + HasMoreChanges bool `json:"hasMoreChanges"` + Created []Identity `json:"created,omitempty"` + Updated []Identity `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` +} + +func (j *Client) GetIdentityChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, sinceState State, maxChanges uint) (IdentityChanges, SessionState, State, Language, Error) { + return changesTemplate(j, "GetIdentityChanges", NS_IDENTITY, + CommandIdentityChanges, CommandIdentityGet, + func() IdentityChangesCommand { + return IdentityChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} + }, + func(path string, rof string) IdentityGetRefCommand { + return IdentityGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandIdentityChanges, + Path: path, + ResultOf: rof, + }, + } + }, + func(resp IdentityChangesResponse) (State, State, bool, []string) { + return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed + }, + func(resp IdentityGetResponse) []Identity { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, created, updated []Identity, destroyed []string) IdentityChanges { + return IdentityChanges{ + OldState: oldState, + NewState: newState, + HasMoreChanges: hasMoreChanges, + Created: created, + Updated: updated, + Destroyed: destroyed, + } + }, + func(resp IdentityGetResponse) State { return resp.State }, + session, ctx, logger, acceptLanguage, + ) +} diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 867bdd2521..58426179c4 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -9,13 +9,15 @@ import ( "github.com/opencloud-eu/opencloud/pkg/structs" ) +var NS_MAILBOX = ns(JmapMail) + type MailboxesResponse struct { Mailboxes []Mailbox `json:"mailboxes"` NotFound []any `json:"notFound"` } func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, State, Language, Error) { - return getTemplate(j, "GetMailbox", CommandCalendarGet, + return getTemplate(j, "GetMailbox", NS_MAILBOX, CommandCalendarGet, func(accountId string, ids []string) MailboxGetCommand { return MailboxGetCommand{AccountId: accountId, Ids: ids} }, @@ -31,7 +33,7 @@ func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Cont } func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Mailbox, SessionState, State, Language, Error) { - return getTemplateN(j, "GetAllMailboxes", CommandCalendarGet, + return getTemplateN(j, "GetAllMailboxes", NS_MAILBOX, CommandCalendarGet, func(accountId string, ids []string) MailboxGetCommand { return MailboxGetCommand{AccountId: accountId} }, @@ -59,7 +61,7 @@ func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx cont }, }, mcid(accountId, "1")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAILBOX, invocations...) if err != nil { return nil, "", "", "", err } @@ -92,7 +94,7 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, invocations[i*len(roles)+j] = invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: MailboxFilterCondition{Role: role}}, mcid(accountId, role)) } } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAILBOX, invocations...) if err != nil { return nil, "", "", "", err } @@ -144,7 +146,7 @@ func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, u // Retrieve Mailbox changes since a given state. // @apidoc mailboxes,changes func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) { - return changesTemplate(j, "GetMailboxChanges", + return changesTemplate(j, "GetMailboxChanges", NS_MAILBOX, CommandMailboxChanges, CommandMailboxGet, func() MailboxChangesCommand { return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} @@ -171,7 +173,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte // 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", + return changesTemplateN(j, "GetMailboxChangesForMultipleAccounts", NS_MAILBOX, accountIds, sinceStateMap, CommandMailboxChanges, CommandMailboxGet, func(accountId string, state State) MailboxChangesCommand { return MailboxChangesCommand{AccountId: accountId, SinceState: state, MaxChanges: posUIntPtr(maxChanges)} @@ -188,91 +190,6 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi func(resp MailboxGetResponse) State { return resp.State }, session, ctx, logger, acceptLanguage, ) - - /* - 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 - } - - invocations := make([]Invocation, n*3) - for i, accountId := range uniqueAccountIds { - changes := MailboxChangesCommand{ - AccountId: accountId, - } - - 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 - } - - 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) { @@ -304,7 +221,7 @@ func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session }, mcid(accountId, "1")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAILBOX, invocations...) if err != nil { return nil, "", "", "", err } @@ -349,7 +266,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *S }, mcid(accountId, "0")) } - cmd, err := j.request(session, logger, invocations...) + cmd, err := j.request(session, logger, NS_MAILBOX, invocations...) if err != nil { return nil, "", "", "", err } @@ -381,7 +298,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *S func (j *Client) UpdateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, ifInState string, update MailboxChange) (Mailbox, SessionState, State, Language, Error) { //NOSONAR logger = j.logger("UpdateMailbox", session, logger) - cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{ + cmd, err := j.request(session, logger, NS_MAILBOX, invocation(CommandMailboxSet, MailboxSetCommand{ AccountId: accountId, IfInState: ifInState, Update: map[string]PatchObject{ @@ -409,7 +326,7 @@ func (j *Client) UpdateMailbox(accountId string, session *Session, ctx context.C func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, create MailboxChange) (Mailbox, SessionState, State, Language, Error) { logger = j.logger("CreateMailbox", session, logger) - cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{ + cmd, err := j.request(session, logger, NS_MAILBOX, invocation(CommandMailboxSet, MailboxSetCommand{ AccountId: accountId, IfInState: ifInState, Create: map[string]MailboxChange{ @@ -441,7 +358,7 @@ func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.C func (j *Client) DeleteMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, mailboxIds []string) ([]string, SessionState, State, Language, Error) { logger = j.logger("DeleteMailbox", session, logger) - cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{ + cmd, err := j.request(session, logger, NS_MAILBOX, invocation(CommandMailboxSet, MailboxSetCommand{ AccountId: accountId, IfInState: ifInState, Destroy: mailboxIds, diff --git a/pkg/jmap/api_objects.go b/pkg/jmap/api_objects.go index 6ab76a0cec..9f507256da 100644 --- a/pkg/jmap/api_objects.go +++ b/pkg/jmap/api_objects.go @@ -6,19 +6,26 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) +var NS_OBJECTS = ns(JmapMail, JmapSubmission, JmapContacts, JmapCalendars, JmapQuota) + 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"` + 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"` + Quotas *QuotaGetResponse `json:"quotas,omitempty"` + Identities *IdentityGetResponse `json:"identities,omitempty"` + EmailSubmissions *EmailSubmissionGetResponse `json:"submissions,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, + quotaIds []string, identityIds []string, + emailSubmissionIds []string, ) (Objects, SessionState, State, Language, Error) { l := j.logger("GetObjects", session, logger).With() if len(mailboxIds) > 0 { @@ -39,6 +46,15 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont if len(eventIds) > 0 { l = l.Array("eventIds", log.SafeStringArray(eventIds)) } + if len(quotaIds) > 0 { + l = l.Array("quotaIds", log.SafeStringArray(quotaIds)) + } + if len(identityIds) > 0 { + l = l.Array("identityIds", log.SafeStringArray(identityIds)) + } + if len(emailSubmissionIds) > 0 { + l = l.Array("emailSubmissionIds", log.SafeStringArray(emailSubmissionIds)) + } logger = log.From(l) methodCalls := []Invocation{} @@ -60,20 +76,31 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont if len(eventIds) > 0 { methodCalls = append(methodCalls, invocation(CommandCalendarEventGet, CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}, "events")) } + if len(quotaIds) > 0 { + methodCalls = append(methodCalls, invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId, Ids: quotaIds}, "quotas")) + } + if len(identityIds) > 0 { + methodCalls = append(methodCalls, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId, Ids: identityIds}, "identities")) + } + if len(emailSubmissionIds) > 0 { + methodCalls = append(methodCalls, invocation(CommandEmailSubmissionGet, EmailSubmissionGetCommand{AccountId: accountId, Ids: emailSubmissionIds}, "emailSubmissionIds")) + } - cmd, err := j.request(session, logger, methodCalls...) + cmd, err := j.request(session, logger, NS_OBJECTS, 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{} + states := map[string]State{} var mailboxes MailboxGetResponse if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandMailboxGet, "mailboxes", &mailboxes); err != nil { return Objects{}, "", err } else if ok { objs.Mailboxes = &mailboxes + states["mailbox"] = mailboxes.State } var emails EmailGetResponse @@ -81,6 +108,7 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont return Objects{}, "", err } else if ok { objs.Emails = &emails + states["email"] = emails.State } var calendars CalendarGetResponse @@ -88,6 +116,7 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont return Objects{}, "", err } else if ok { objs.Calendars = &calendars + states["calendar"] = calendars.State } var events CalendarEventGetResponse @@ -95,6 +124,7 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont return Objects{}, "", err } else if ok { objs.Events = &events + states["event"] = events.State } var addressbooks AddressBookGetResponse @@ -102,6 +132,7 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont return Objects{}, "", err } else if ok { objs.Addressbooks = &addressbooks + states["addressbook"] = addressbooks.State } var contacts ContactCardGetResponse @@ -109,10 +140,33 @@ func (j *Client) GetObjects(accountId string, session *Session, ctx context.Cont return Objects{}, "", err } else if ok { objs.Contacts = &contacts + states["contact"] = contacts.State } - state := squashStates(mailboxes.State, emails.State, calendars.State, events.State, addressbooks.State, contacts.State) + var quotas QuotaGetResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandQuotaGet, "quotas", "as); err != nil { + return Objects{}, "", err + } else if ok { + objs.Quotas = "as + states["quota"] = quotas.State + } - return objs, state, nil + var identities IdentityGetResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandIdentityGet, "identities", &identities); err != nil { + return Objects{}, "", err + } else if ok { + objs.Identities = &identities + states["identity"] = identities.State + } + + var submissions EmailSubmissionGetResponse + if ok, err := tryRetrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "submissions", &submissions); err != nil { + return Objects{}, "", err + } else if ok { + objs.EmailSubmissions = &submissions + states["submissions"] = submissions.State + } + + return objs, squashKeyedStates(states), nil }) } diff --git a/pkg/jmap/api_quota.go b/pkg/jmap/api_quota.go index 0f137650ef..367d152384 100644 --- a/pkg/jmap/api_quota.go +++ b/pkg/jmap/api_quota.go @@ -6,8 +6,10 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) +var NS_QUOTA = ns(JmapQuota) + func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]QuotaGetResponse, SessionState, State, Language, Error) { - return getTemplateN(j, "GetQuotas", CommandQuotaGet, + return getTemplateN(j, "GetQuotas", NS_QUOTA, CommandQuotaGet, func(accountId string, ids []string) QuotaGetCommand { return QuotaGetCommand{AccountId: accountId} }, @@ -17,3 +19,87 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co accountIds, session, ctx, logger, acceptLanguage, []string{}, ) } + +type QuotaChanges struct { + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState"` + HasMoreChanges bool `json:"hasMoreChanges"` + Created []Quota `json:"created,omitempty"` + Updated []Quota `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` +} + +func (j *Client) GetQuotaChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, sinceState State, maxChanges uint) (QuotaChanges, SessionState, State, Language, Error) { + return changesTemplate(j, "GetQuotaChanges", NS_QUOTA, + CommandQuotaChanges, CommandQuotaGet, + func() QuotaChangesCommand { + return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} + }, + func(path string, rof string) QuotaGetRefCommand { + return QuotaGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandQuotaChanges, + Path: path, + ResultOf: rof, + }, + } + }, + func(resp QuotaChangesResponse) (State, State, bool, []string) { + return resp.OldState, resp.NewState, resp.HasMoreChanges, resp.Destroyed + }, + func(resp QuotaGetResponse) []Quota { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, created, updated []Quota, destroyed []string) QuotaChanges { + return QuotaChanges{ + OldState: oldState, + NewState: newState, + HasMoreChanges: hasMoreChanges, + Created: created, + Updated: updated, + Destroyed: destroyed, + } + }, + func(resp QuotaGetResponse) State { return resp.State }, + session, ctx, logger, acceptLanguage, + ) +} + +func (j *Client) GetQuotaUsageChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, + acceptLanguage string, sinceState State, maxChanges uint) (QuotaChanges, SessionState, State, Language, Error) { + return updatedTemplate(j, "GetQuotaUsageChanges", NS_QUOTA, + CommandQuotaChanges, CommandQuotaGet, + func() QuotaChangesCommand { + return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)} + }, + func(path string, rof string) QuotaGetRefCommand { + return QuotaGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + Name: CommandQuotaChanges, + Path: path, + ResultOf: rof, + }, + PropertiesRef: &ResultReference{ + Name: CommandQuotaChanges, + Path: "/updatedProperties", + ResultOf: rof, + }, + } + }, + func(resp QuotaChangesResponse) (State, State, bool) { + return resp.OldState, resp.NewState, resp.HasMoreChanges + }, + func(resp QuotaGetResponse) []Quota { return resp.List }, + func(oldState, newState State, hasMoreChanges bool, updated []Quota) QuotaChanges { + return QuotaChanges{ + OldState: oldState, + NewState: newState, + HasMoreChanges: hasMoreChanges, + Updated: updated, + } + }, + func(resp QuotaGetResponse) State { return resp.State }, + session, ctx, logger, acceptLanguage, + ) +} diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go index 8b0073c373..8eeeae75f4 100644 --- a/pkg/jmap/api_vacation.go +++ b/pkg/jmap/api_vacation.go @@ -8,12 +8,14 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) +var NS_VACATION = ns(JmapVacationResponse) + const ( vacationResponseId = "singleton" ) func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseGetResponse, SessionState, State, Language, Error) { - return getTemplate(j, "GetVacationResponse", CommandVacationResponseGet, + return getTemplate(j, "GetVacationResponse", NS_VACATION, CommandVacationResponseGet, func(accountId string, ids []string) VacationResponseGetCommand { return VacationResponseGetCommand{AccountId: accountId} }, @@ -50,7 +52,7 @@ type VacationResponsePayload struct { func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponse, SessionState, State, Language, Error) { logger = j.logger("SetVacationResponse", session, logger) - cmd, err := j.request(session, logger, + cmd, err := j.request(session, logger, NS_VACATION, invocation(CommandVacationResponseSet, VacationResponseSetCommand{ AccountId: accountId, Create: map[string]VacationResponse{ diff --git a/pkg/jmap/client.go b/pkg/jmap/client.go index d3814c0077..81b05bfccf 100644 --- a/pkg/jmap/client.go +++ b/pkg/jmap/client.go @@ -5,6 +5,7 @@ import ( "errors" "io" "net/url" + "slices" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/rs/zerolog" @@ -91,13 +92,20 @@ func (j *Client) maxCallsCheck(calls int, session *Session, logger *log.Logger) // Construct a Request from the given list of Invocation objects. // // If an issue occurs, then it is logged prior to returning it. -func (j *Client) request(session *Session, logger *log.Logger, methodCalls ...Invocation) (Request, Error) { +func (j *Client) request(session *Session, logger *log.Logger, using []JmapNamespace, methodCalls ...Invocation) (Request, Error) { err := j.maxCallsCheck(len(methodCalls), session, logger) if err != nil { return Request{}, err } + + if using == nil { + using = JmapNamespaces + } + if !slices.Contains(using, JmapCore) { + using = slices.Insert(using, 0, JmapCore) + } return Request{ - Using: []string{JmapCore, JmapMail, JmapContacts}, + Using: using, MethodCalls: methodCalls, CreatedIds: nil, }, nil diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go index ae720325ed..73fb53fffa 100644 --- a/pkg/jmap/integration_test.go +++ b/pkg/jmap/integration_test.go @@ -708,9 +708,9 @@ func (j *TestJmapClient) create(id string, objectType ObjectType, body map[strin }).command(body) } -func (j *TestJmapClient) create1(accountId string, objectType ObjectType, ns string, obj map[string]any) (string, error) { +func (j *TestJmapClient) create1(accountId string, objectType ObjectType, ns JmapNamespace, obj map[string]any) (string, error) { body := map[string]any{ - "using": []string{JmapCore, ns}, + "using": []string{string(JmapCore), string(ns)}, "methodCalls": []any{ []any{ objectType + "/set", @@ -727,11 +727,11 @@ func (j *TestJmapClient) create1(accountId string, objectType ObjectType, ns str return j.create("c", objectType, body) } -func (j *TestJmapClient) objectsById(accountId string, objectType ObjectType, scope string) (map[string]map[string]any, error) { +func (j *TestJmapClient) objectsById(accountId string, objectType ObjectType, scope JmapNamespace) (map[string]map[string]any, error) { m := map[string]map[string]any{} { body := map[string]any{ - "using": []string{JmapCore, scope}, + "using": []string{string(JmapCore), string(scope)}, "methodCalls": []any{ []any{ objectType + "/get", diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index a85bda3378..7a9efaf715 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -89,26 +89,28 @@ type DispositionTypeOption string type Duration string +type JmapNamespace string + const ( - JmapCore = "urn:ietf:params:jmap:core" - JmapMail = "urn:ietf:params:jmap:mail" - JmapMDN = "urn:ietf:params:jmap:mdn" // https://datatracker.ietf.org/doc/rfc9007/ - JmapSubmission = "urn:ietf:params:jmap:submission" - JmapVacationResponse = "urn:ietf:params:jmap:vacationresponse" - JmapCalendars = "urn:ietf:params:jmap:calendars" - JmapContacts = "urn:ietf:params:jmap:contacts" - JmapSieve = "urn:ietf:params:jmap:sieve" - JmapBlob = "urn:ietf:params:jmap:blob" - JmapQuota = "urn:ietf:params:jmap:quota" - JmapWebsocket = "urn:ietf:params:jmap:websocket" // #nosec G101 false positive: these are not credentials - JmapPrincipals = "urn:ietf:params:jmap:principals" - JmapPrincipalsOwner = "urn:ietf:params:jmap:principals:owner" - JmapTasks = "urn:ietf:params:jmap:tasks" - JmapTasksRecurrences = "urn:ietf:params:jmap:tasks:recurrences" - JmapTasksAssignees = "urn:ietf:params:jmap:tasks:assignees" - JmapTasksAlerts = "urn:ietf:params:jmap:tasks:alerts" - JmapTasksMultilingual = "urn:ietf:params:jmap:tasks:multilingual" - JmapTasksCustomTimezones = "urn:ietf:params:jmap:tasks:customtimezones" + JmapCore = JmapNamespace("urn:ietf:params:jmap:core") + JmapMail = JmapNamespace("urn:ietf:params:jmap:mail") + JmapMDN = JmapNamespace("urn:ietf:params:jmap:mdn") // https://datatracker.ietf.org/doc/rfc9007/ + JmapSubmission = JmapNamespace("urn:ietf:params:jmap:submission") + JmapVacationResponse = JmapNamespace("urn:ietf:params:jmap:vacationresponse") + JmapCalendars = JmapNamespace("urn:ietf:params:jmap:calendars") + JmapContacts = JmapNamespace("urn:ietf:params:jmap:contacts") + JmapSieve = JmapNamespace("urn:ietf:params:jmap:sieve") + JmapBlob = JmapNamespace("urn:ietf:params:jmap:blob") + JmapQuota = JmapNamespace("urn:ietf:params:jmap:quota") + JmapWebsocket = JmapNamespace("urn:ietf:params:jmap:websocket") // #nosec G101 false positive: these are not credentials + JmapPrincipals = JmapNamespace("urn:ietf:params:jmap:principals") + JmapPrincipalsOwner = JmapNamespace("urn:ietf:params:jmap:principals:owner") + JmapTasks = JmapNamespace("urn:ietf:params:jmap:tasks") + JmapTasksRecurrences = JmapNamespace("urn:ietf:params:jmap:tasks:recurrences") + JmapTasksAssignees = JmapNamespace("urn:ietf:params:jmap:tasks:assignees") + JmapTasksAlerts = JmapNamespace("urn:ietf:params:jmap:tasks:alerts") + JmapTasksMultilingual = JmapNamespace("urn:ietf:params:jmap:tasks:multilingual") + JmapTasksCustomTimezones = JmapNamespace("urn:ietf:params:jmap:tasks:customtimezones") CoreType = ObjectType("Core") PushSubscriptionType = ObjectType("PushSubscription") @@ -260,6 +262,28 @@ const ( ) var ( + JmapNamespaces = []JmapNamespace{ + JmapCore, + JmapMail, + JmapMDN, + JmapSubmission, + JmapVacationResponse, + JmapCalendars, + JmapContacts, + JmapSieve, + JmapBlob, + JmapQuota, + JmapWebsocket, + JmapPrincipals, + JmapPrincipalsOwner, + JmapTasks, + JmapTasksRecurrences, + JmapTasksAssignees, + JmapTasksAlerts, + JmapTasksMultilingual, + JmapTasksCustomTimezones, + } + ObjectTypes = []ObjectType{ CoreType, PushSubscriptionType, @@ -1334,29 +1358,6 @@ type MailboxSetResponse struct { NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` } -type MailboxChangesCommand 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 Mailbox/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 MailboxFilterElement interface { _isAMailboxFilterElement() // marker method } @@ -2490,7 +2491,7 @@ type EmailSubmissionGetRefCommand struct { // // If null, then all records of the data type are returned, if this is supported for that data // type and the number of records does not exceed the maxObjectsInGet limit. - IdRef *ResultReference `json:"#ids,omitempty"` + IdsRef *ResultReference `json:"#ids,omitempty"` // If supplied, only the properties listed in the array are returned for each EmailSubmission object. // @@ -2532,6 +2533,54 @@ type EmailSubmissionGetResponse struct { NotFound []string `json:"notFound,omitempty"` } +type EmailSubmissionChangesCommand 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 EmailSubmission/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 EmailSubmissionChangesResponse 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 EmailSubmission/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"` +} + // Patch Object. // // Example: @@ -2643,7 +2692,7 @@ type Request struct { // The client MAY include capability identifiers even if the method calls it makes do not utilise those capabilities. // The server advertises the set of specifications it supports in the Session object // (see [Section 2](https://jmap.io/spec-core.html#the-jmap-session-resource)), as keys on the capabilities property. - Using []string `json:"using"` + Using []JmapNamespace `json:"using"` // An array of method calls to process on the server. // @@ -2815,6 +2864,29 @@ type MailboxGetResponse struct { NotFound []any `json:"notFound"` } +type MailboxChangesCommand 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 Mailbox/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 MailboxChangesResponse struct { // The id of the account used for the call. AccountId string `json:"accountId"` @@ -3209,6 +3281,60 @@ type IdentityGetCommand struct { Ids []string `json:"ids,omitempty"` } +type IdentityGetRefCommand struct { + AccountId string `json:"accountId"` + IdsRef *ResultReference `json:"#ids,omitempty"` + PropertiesRef *ResultReference `json:"#properties,omitempty"` +} + +type IdentityChangesCommand 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 Mailbox/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 IdentityChangesResponse 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 IdentitySetCommand struct { AccountId string `json:"accountId"` IfInState string `json:"ifInState,omitempty"` @@ -4948,6 +5074,12 @@ type QuotaGetCommand struct { Ids []string `json:"ids,omitempty"` } +type QuotaGetRefCommand struct { + AccountId string `json:"accountId"` + IdsRef *ResultReference `json:"#ids,omitempty"` + PropertiesRef *ResultReference `json:"#properties,omitempty"` +} + type QuotaGetResponse struct { AccountId string `json:"accountId"` State State `json:"state,omitempty"` @@ -4955,6 +5087,52 @@ type QuotaGetResponse struct { NotFound []string `json:"notFound,omitempty"` } +type QuotaChangesCommand 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 "Quota/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,omitempty"` + + // If only the "used" Quota property has changed since the old state, this will be a list containing only that property. + // + // If the server is unable to tell if only "used" has changed, it MUST be null. + UpdatedProperties []string `json:"updatedProperties,omitempty"` +} + +type QuotaChangesResponse 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 "Quota/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 AddressBookGetCommand struct { AccountId string `json:"accountId"` Ids []string `json:"ids,omitempty"` @@ -6067,74 +6245,80 @@ 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" - 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" + 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" + CommandEmailSubmissionChanges Command = "EmailSubmission/changes" + 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" + CommandIdentityChanges Command = "Identity/changes" + CommandVacationResponseGet Command = "VacationResponse/get" + CommandVacationResponseSet Command = "VacationResponse/set" + CommandSearchSnippetGet Command = "SearchSnippet/get" + CommandQuotaGet Command = "Quota/get" + CommandQuotaChanges Command = "Quota/changes" + 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{} }, - 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{} }, + 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{} }, + CommandEmailSubmissionChanges: func() any { return EmailSubmissionChangesResponse{} }, + CommandThreadGet: func() any { return ThreadGetResponse{} }, + CommandIdentityGet: func() any { return IdentityGetResponse{} }, + CommandIdentityChanges: func() any { return IdentityChangesResponse{} }, + 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{} }, + CommandQuotaChanges: func() any { return QuotaChangesResponse{} }, + 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{} }, } diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 802f54ffd5..3fb02805c3 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -10,8 +10,8 @@ import ( ) func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR - client *Client, name string, getCommand Command, - getCommandFactory func(string, []string) GETREQ, + client *Client, name string, using []JmapNamespace, + getCommand Command, getCommandFactory func(string, []string) GETREQ, mapper func(GETRESP) RESP, stateMapper func(GETRESP) State, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) { @@ -19,7 +19,7 @@ func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR var zero RESP - cmd, err := client.request(session, logger, + cmd, err := client.request(session, logger, using, invocation(getCommand, getCommandFactory(accountId, ids), "0"), ) if err != nil { @@ -38,8 +38,8 @@ func getTemplate[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, + client *Client, name string, using []JmapNamespace, + getCommand Command, getCommandFactory func(string, []string) GETREQ, itemMapper func(GETRESP) ITEM, respMapper func(map[string]ITEM) RESP, stateMapper func(GETRESP) State, @@ -55,7 +55,7 @@ func getTemplateN[GETREQ any, GETRESP any, ITEM any, RESP any]( //NOSONAR invocations[i] = invocation(getCommand, getCommandFactory(accountId, ids), mcid(accountId, "0")) } - cmd, err := client.request(session, logger, invocations...) + cmd, err := client.request(session, logger, using, invocations...) if err != nil { return zero, "", "", "", err } @@ -77,7 +77,8 @@ func getTemplateN[GETREQ any, GETRESP any, ITEM any, RESP any]( //NOSONAR } func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( //NOSONAR - client *Client, name string, t ObjectType, setCommand Command, getCommand Command, + client *Client, name string, using []JmapNamespace, t ObjectType, + setCommand Command, getCommand Command, setCommandFactory func(string, map[string]T) SETREQ, getCommandFactory func(string, string) GETREQ, createdMapper func(SETRESP) map[string]*T, @@ -88,7 +89,7 @@ func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( // logger = client.logger(name, session, logger) createMap := map[string]T{"c": create} - cmd, err := client.request(session, logger, + cmd, err := client.request(session, logger, using, invocation(setCommand, setCommandFactory(accountId, createMap), "0"), invocation(getCommand, getCommandFactory(accountId, "#c"), "1"), ) @@ -135,14 +136,14 @@ func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( // }) } -func deleteTemplate[REQ any, RESP any](client *Client, name string, c Command, //NOSONAR - commandFactory func(string, []string) REQ, +func deleteTemplate[REQ any, RESP any](client *Client, name string, using []JmapNamespace, //NOSONAR + c Command, commandFactory func(string, []string) REQ, notDestroyedMapper func(RESP) map[string]SetError, stateMapper func(RESP) State, accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { logger = client.logger(name, session, logger) - cmd, err := client.request(session, logger, + cmd, err := client.request(session, logger, using, invocation(c, commandFactory(accountId, destroy), "0"), ) if err != nil { @@ -160,7 +161,7 @@ func deleteTemplate[REQ any, RESP any](client *Client, name string, c Command, / } func changesTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, RESP any]( //NOSONAR - client *Client, name string, + client *Client, name string, using []JmapNamespace, changesCommand Command, getCommand Command, changesCommandFactory func() CHANGESREQ, getCommandFactory func(string, string) GETREQ, @@ -175,7 +176,7 @@ func changesTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, I changes := changesCommandFactory() getCreated := getCommandFactory("/created", "0") //NOSONAR getUpdated := getCommandFactory("/updated", "0") //NOSONAR - cmd, err := client.request(session, logger, + cmd, err := client.request(session, logger, using, invocation(changesCommand, changes, "0"), invocation(getCommand, getCreated, "1"), invocation(getCommand, getUpdated, "2"), @@ -215,8 +216,54 @@ func changesTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, I }) } +func updatedTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, RESP any]( //NOSONAR + client *Client, name string, using []JmapNamespace, + changesCommand Command, getCommand Command, + changesCommandFactory func() CHANGESREQ, + getCommandFactory func(string, string) GETREQ, + changesMapper func(CHANGESRESP) (State, State, bool), + getMapper func(GETRESP) []ITEM, + respMapper func(State, State, bool, []ITEM) 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() + getUpdated := getCommandFactory("/updated", "0") //NOSONAR + cmd, err := client.request(session, logger, using, + invocation(changesCommand, changes, "0"), + invocation(getCommand, getUpdated, "1"), + ) + 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 updatedResponse GETRESP + err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &updatedResponse) + if err != nil { + logger.Error().Err(err).Send() + return zero, "", err + } + + oldState, newState, hasMoreChanges := changesMapper(changesResponse) + updated := getMapper(updatedResponse) + + result := respMapper(oldState, newState, hasMoreChanges, updated) + + return result, stateMapper(updatedResponse), nil + }) +} + func changesTemplateN[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, CHANGESITEM any, RESP any]( //NOSONAR - client *Client, name string, + client *Client, name string, using []JmapNamespace, accountIds []string, sinceStateMap map[string]State, changesCommand Command, getCommand Command, changesCommandFactory func(string, State) CHANGESREQ, @@ -260,7 +307,7 @@ func changesTemplateN[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, invocations[i*3+2] = invocation(getCommand, getUpdated, mcid(accountId, "2")) } - cmd, err := client.request(session, logger, invocations...) + cmd, err := client.request(session, logger, using, invocations...) if err != nil { return zero, "", "", "", err } diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index 7f527412ee..284f789221 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -382,3 +382,12 @@ func posUIntPtr(i uint) *uint { return nil } } + +func ns(namespaces ...JmapNamespace) []JmapNamespace { + result := make([]JmapNamespace, len(namespaces)+1) + result[0] = JmapCore + for i, n := range namespaces { + result[i+1] = n + } + return result +} diff --git a/services/groupware/pkg/groupware/api_changes.go b/services/groupware/pkg/groupware/api_changes.go index 986d7dce0b..cec3d3f6a7 100644 --- a/services/groupware/pkg/groupware/api_changes.go +++ b/services/groupware/pkg/groupware/api_changes.go @@ -61,6 +61,13 @@ func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSON if state, ok := req.getStringParam(QueryParamEvents, ""); ok { sinceState.Events = ptr(toState(state)) } + if state, ok := req.getStringParam(QueryParamIdentities, ""); ok { + sinceState.Identities = ptr(toState(state)) + } + if state, ok := req.getStringParam(QueryParamEmailSubmissions, ""); ok { + sinceState.EmailSubmissions = ptr(toState(state)) + } + //if state, ok := req.getStringParam(QueryParamQuotas, ""); ok { sinceState.Quotas = ptr(toState(state)) } if sinceState.IsZero() { return req.noop(accountId) } diff --git a/services/groupware/pkg/groupware/api_objects.go b/services/groupware/pkg/groupware/api_objects.go index 0c35e95cbb..c93d1b3a40 100644 --- a/services/groupware/pkg/groupware/api_objects.go +++ b/services/groupware/pkg/groupware/api_objects.go @@ -8,12 +8,15 @@ import ( ) 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"` + 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"` + Quotas []string `json:"quotas,omitempty"` + Identities []string `json:"identities,omitempty"` + EmailSubmissions []string `json:"submissions,omitempty"` } // Retrieve changes for multiple or all Groupware objects, based on their respective state token. @@ -48,17 +51,23 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON contactIds := []string{} calendarIds := []string{} eventIds := []string{} + quotaIds := []string{} + identityIds := []string{} + emailSubmissionIds := []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...) + emailIds = append(emailIds, objects.Emails...) + addressbookIds = append(addressbookIds, objects.Addressbooks...) + contactIds = append(contactIds, objects.Contacts...) + calendarIds = append(calendarIds, objects.Calendars...) + eventIds = append(eventIds, objects.Events...) + quotaIds = append(quotaIds, objects.Quotas...) + identityIds = append(identityIds, objects.Identities...) + emailSubmissionIds = append(emailSubmissionIds, objects.EmailSubmissions...) } } @@ -92,10 +101,25 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON } else if ok { eventIds = append(eventIds, list...) } + if list, ok, err := req.parseOptStringListParam(QueryParamQuotas); err != nil { + return req.error(accountId, err) + } else if ok { + quotaIds = append(quotaIds, list...) + } + if list, ok, err := req.parseOptStringListParam(QueryParamIdentities); err != nil { + return req.error(accountId, err) + } else if ok { + identityIds = append(identityIds, list...) + } + if list, ok, err := req.parseOptStringListParam(QueryParamEmailSubmissions); err != nil { + return req.error(accountId, err) + } else if ok { + emailSubmissionIds = append(emailSubmissionIds, 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) + mailboxIds, emailIds, addressbookIds, contactIds, calendarIds, eventIds, quotaIds, identityIds, emailSubmissionIds) if jerr != nil { return req.jmapError(accountId, jerr, sessionState, lang) } diff --git a/services/groupware/pkg/groupware/api_quota.go b/services/groupware/pkg/groupware/api_quota.go index bfdbdeeabd..bbf6cc7947 100644 --- a/services/groupware/pkg/groupware/api_quota.go +++ b/services/groupware/pkg/groupware/api_quota.go @@ -64,3 +64,36 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state) }) } + +// Get changes to Contacts since a given State +// @api:tags contact,changes +func (g *Groupware) GetQuotaChanges(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + accountId, err := req.GetAccountIdForQuota() + if err != nil { + return req.error(accountId, err) + } + + l := req.logger.With().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.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list quota changes")) + l = l.Str(HeaderParamSince, log.SafeString(string(sinceState))) + + logger := log.From(l) + changes, sessionState, state, lang, jerr := g.jmap.GetQuotaChanges(accountId, req.session, req.ctx, logger, req.language(), sinceState, maxChanges) + if jerr != nil { + return req.jmapError(accountId, jerr, sessionState, lang) + } + var body jmap.QuotaChanges = changes + + return req.respond(accountId, body, sessionState, QuotaResponseObjectType, state) + }) +} diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go index 0275705cfe..57bca79502 100644 --- a/services/groupware/pkg/groupware/route.go +++ b/services/groupware/pkg/groupware/route.go @@ -64,6 +64,9 @@ const ( QueryParamContacts = "contacts" QueryParamCalendars = "calendars" QueryParamEvents = "events" + QueryParamQuotas = "quotas" + QueryParamIdentities = "identities" + QueryParamEmailSubmissions = "submissions" HeaderParamSince = "if-none-match" )