groupware: refactoring using function templates

* adds creating addressbooks, calendars, mailboxes

 * adds deleting mailbox, event, identity

 * adds modifying an email

 * introduce template functions for the Groupware API in templates.go,
   and use those in route function implementations whenever possible

 * add capability checking for mail, quota, blobs

 * adds Changes interface

 * adds JmapResponse interface
This commit is contained in:
Pascal Bleser
2026-04-20 10:17:14 +02:00
parent 38f7e24947
commit a115679f3d
42 changed files with 1326 additions and 1506 deletions

View File

@@ -14,7 +14,16 @@ func (j *Client) GetAddressbooks(accountId string, ids []string, ctx Context) (A
)
}
type AddressBookChanges = ChangesTemplate[AddressBook]
type AddressBookChanges ChangesTemplate[AddressBook]
var _ Changes[AddressBook] = AddressBookChanges{}
func (c AddressBookChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c AddressBookChanges) GetOldState() State { return c.OldState }
func (c AddressBookChanges) GetNewState() State { return c.NewState }
func (c AddressBookChanges) GetCreated() []AddressBook { return c.Created }
func (c AddressBookChanges) GetUpdated() []AddressBook { return c.Updated }
func (c AddressBookChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Address Book changes since a given state.
// @apidoc addressbook,changes

View File

@@ -10,10 +10,10 @@ import (
var NS_BLOB = ns(JmapBlob)
func (j *Client) GetBlobMetadata(accountId string, id string, ctx Context) (*Blob, SessionState, State, Language, Error) {
func (j *Client) GetBlobMetadata(accountId string, ids []string, ctx Context) (BlobGetResponse, SessionState, State, Language, Error) {
get := BlobGetCommand{
AccountId: accountId,
Ids: []string{id},
Ids: ids,
// add BlobPropertyData to retrieve the data
Properties: []string{BlobPropertyDigestSha256, BlobPropertyDigestSha512, BlobPropertySize},
}
@@ -21,22 +21,16 @@ func (j *Client) GetBlobMetadata(accountId string, id string, ctx Context) (*Blo
invocation(get, "0"),
)
if jerr != nil {
return nil, "", "", "", jerr
return bail[BlobGetResponse](jerr)
}
return command(j, ctx, cmd, func(body *Response) (*Blob, State, Error) {
return command(j, ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) {
var response BlobGetResponse
err := retrieveGet(ctx, body, get, "0", &response)
if err != nil {
return nil, "", err
return BlobGetResponse{}, EmptyState, err
}
if len(response.List) != 1 {
ctx.Logger.Error().Msgf("%T.List has %v entries instead of 1", response, len(response.List))
return nil, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
}
get := response.List[0]
return &get, response.State, nil
return response, response.State, nil
})
}

View File

@@ -35,7 +35,16 @@ func (j *Client) GetCalendars(accountId string, ids []string, ctx Context) (Cale
)
}
type CalendarChanges = ChangesTemplate[Calendar]
type CalendarChanges ChangesTemplate[Calendar]
var _ Changes[Calendar] = CalendarChanges{}
func (c CalendarChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c CalendarChanges) GetOldState() State { return c.OldState }
func (c CalendarChanges) GetNewState() State { return c.NewState }
func (c CalendarChanges) GetCreated() []Calendar { return c.Created }
func (c CalendarChanges) GetUpdated() []Calendar { return c.Updated }
func (c CalendarChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve Calendar changes since a given state.
// @apidoc calendar,changes

View File

@@ -10,7 +10,7 @@ import (
var NS_CHANGES = ns(JmapMail, JmapContacts, JmapCalendars) //, JmapQuota)
type Changes struct {
type ObjectChanges struct {
MaxChanges uint `json:"maxchanges,omitzero"`
Mailboxes *MailboxChangesResponse `json:"mailboxes,omitempty"`
Emails *EmailChangesResponse `json:"emails,omitempty"`
@@ -74,7 +74,7 @@ func (s StateMap) MarshalZerologObject(e *zerolog.Event) {
// Retrieve the changes in any type of objects at once since a given State.
// @api:tags changes
func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint, ctx Context) (Changes, SessionState, State, Language, Error) { //NOSONAR
func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint, ctx Context) (ObjectChanges, SessionState, State, Language, Error) { //NOSONAR
logger := log.From(j.logger("GetChanges", ctx).With().Object("state", stateMap).Uint("maxChanges", maxChanges))
ctx = ctx.WithLogger(logger)
@@ -107,18 +107,18 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
cmd, err := j.request(ctx, NS_CHANGES, methodCalls...)
if err != nil {
return Changes{}, "", "", "", err
return ObjectChanges{}, "", "", "", err
}
return command(j, ctx, cmd, func(body *Response) (Changes, State, Error) {
changes := Changes{
return command(j, ctx, cmd, func(body *Response) (ObjectChanges, State, Error) {
changes := ObjectChanges{
MaxChanges: maxChanges,
}
states := map[string]State{}
var mailboxes MailboxChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandMailboxChanges, "mailboxes", &mailboxes); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Mailboxes = &mailboxes
states["mailbox"] = mailboxes.NewState
@@ -126,7 +126,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var emails EmailChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailChanges, "emails", &emails); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Emails = &emails
states["emails"] = emails.NewState
@@ -134,7 +134,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var calendars CalendarChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarChanges, "calendars", &calendars); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Calendars = &calendars
states["calendars"] = calendars.NewState
@@ -142,7 +142,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var events CalendarEventChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandCalendarEventChanges, "events", &events); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Events = &events
states["events"] = events.NewState
@@ -150,7 +150,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var addressbooks AddressBookChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandAddressBookChanges, "addressbooks", &addressbooks); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Addressbooks = &addressbooks
states["addressbooks"] = addressbooks.NewState
@@ -158,7 +158,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var contacts ContactCardChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandContactCardChanges, "contacts", &contacts); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Contacts = &contacts
states["contacts"] = contacts.NewState
@@ -166,7 +166,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var identities IdentityChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandIdentityChanges, "identities", &identities); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.Identities = &identities
states["identities"] = identities.NewState
@@ -174,7 +174,7 @@ func (j *Client) GetChanges(accountId string, stateMap StateMap, maxChanges uint
var submissions EmailSubmissionChangesResponse
if ok, err := tryRetrieveResponseMatchParameters(ctx, body, CommandEmailSubmissionChanges, "submissions", &submissions); err != nil {
return Changes{}, "", err
return ObjectChanges{}, "", err
} else if ok {
changes.EmailSubmissions = &submissions
states["submissions"] = submissions.NewState

View File

@@ -1,7 +1,11 @@
package jmap
import "github.com/opencloud-eu/opencloud/pkg/jscontact"
var NS_CONTACTS = ns(JmapContacts)
var DEFAULT_CONTACT_CARD_VERSION = jscontact.JSContactVersion_1_0
func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Context) (ContactCardGetResponse, SessionState, State, Language, Error) {
return get(j, "GetContactCards", ContactCardType,
func(accountId string, ids []string) ContactCardGetCommand {
@@ -14,7 +18,16 @@ func (j *Client) GetContactCards(accountId string, contactIds []string, ctx Cont
)
}
type ContactCardChanges = ChangesTemplate[ContactCard]
type ContactCardChanges ChangesTemplate[ContactCard]
var _ Changes[ContactCard] = ContactCardChanges{}
func (c ContactCardChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c ContactCardChanges) GetOldState() State { return c.OldState }
func (c ContactCardChanges) GetNewState() State { return c.NewState }
func (c ContactCardChanges) GetCreated() []ContactCard { return c.Created }
func (c ContactCardChanges) GetUpdated() []ContactCard { return c.Updated }
func (c ContactCardChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Contact Cards since a given State.
// @api:tags contact,changes
@@ -85,9 +98,12 @@ func (j *Client) QueryContactCards(accountIds []string,
)
}
func (j *Client) CreateContactCard(accountId string, contact ContactCard, ctx Context) (*ContactCard, SessionState, State, Language, Error) {
func (j *Client) CreateContactCard(accountId string, contact ContactCardChange, ctx Context) (*ContactCard, SessionState, State, Language, Error) {
if contact.Version == nil {
contact.Version = &DEFAULT_CONTACT_CARD_VERSION
}
return create(j, "CreateContactCard", ContactCardType,
func(accountId string, create map[string]ContactCard) ContactCardSetCommand {
func(accountId string, create map[string]ContactCardChange) ContactCardSetCommand {
return ContactCardSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ids string) ContactCardGetCommand {

View File

@@ -34,9 +34,9 @@ func (j *Client) GetEmails(accountId string, ids []string, //NOSONAR
methodCalls := []Invocation{invokeGet}
var markEmails EmailSetCommand
if markAsSeen {
updates := make(map[string]EmailUpdate, len(ids))
updates := make(map[string]PatchObject, len(ids))
for _, id := range ids {
updates[id] = EmailUpdate{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
updates[id] = PatchObject{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
}
markEmails = EmailSetCommand{AccountId: accountId, Update: updates}
methodCalls = []Invocation{invocation(markEmails, "0"), invokeGet}
@@ -111,7 +111,15 @@ func (j *Client) GetEmailBlobId(accountId string, id string, ctx Context) (strin
})
}
type EmailSearchResults = SearchResultsTemplate[Email]
type EmailSearchResults SearchResultsTemplate[Email]
var _ SearchResults[Email] = EmailSearchResults{}
func (r EmailSearchResults) GetResults() []Email { return r.Results }
func (r EmailSearchResults) GetCanCalculateChanges() bool { return r.CanCalculateChanges }
func (r EmailSearchResults) GetPosition() uint { return r.Position }
func (r EmailSearchResults) GetLimit() uint { return r.Limit }
func (r EmailSearchResults) GetTotal() *uint { return r.Total }
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR
@@ -200,14 +208,16 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS
})
}
type EmailChanges struct {
HasMoreChanges bool `json:"hasMoreChanges"`
OldState State `json:"oldState,omitempty"`
NewState State `json:"newState"`
Created []Email `json:"created,omitempty"`
Updated []Email `json:"updated,omitempty"`
Destroyed []string `json:"destroyed,omitempty"`
}
type EmailChanges ChangesTemplate[Email]
var _ Changes[Email] = EmailChanges{}
func (c EmailChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c EmailChanges) GetOldState() State { return c.OldState }
func (c EmailChanges) GetNewState() State { return c.NewState }
func (c EmailChanges) GetCreated() []Email { return c.Created }
func (c EmailChanges) GetUpdated() []Email { return c.Updated }
func (c EmailChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Emails since a given State.
// @api:tags email,changes
@@ -495,13 +505,13 @@ type EmailWithSnippets struct {
type EmailQueryWithSnippetsResult struct {
Results []EmailWithSnippets `json:"results"`
Total uint `json:"total"`
Position uint `json:"position"`
Limit uint `json:"limit,omitzero"`
Position uint `json:"position,omitzero"`
QueryState State `json:"queryState"`
}
func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR
filter EmailFilterElement, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint,
filter EmailFilterElement, offset int, limit uint, collapseThreads bool, calculateTotal bool, fetchBodies bool, maxBodyValueBytes uint,
ctx Context) (map[string]EmailQueryWithSnippetsResult, SessionState, State, Language, Error) {
logger := j.loggerParams("QueryEmailsWithSnippets", ctx, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
@@ -515,8 +525,8 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, //NOSONAR
AccountId: accountId,
Filter: filter,
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
CollapseThreads: false,
CalculateTotal: true,
CollapseThreads: collapseThreads,
CalculateTotal: calculateTotal,
}
if offset > 0 {
query.Position = offset
@@ -689,10 +699,10 @@ func (j *Client) ImportEmail(accountId string, data []byte, ctx Context) (Upload
}
func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId string, ctx Context) (*Email, SessionState, State, Language, Error) {
func (j *Client) CreateEmail(accountId string, email EmailChange, replaceId string, ctx Context) (*Email, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
Create: map[string]EmailCreate{
Create: map[string]EmailChange{
"c": email,
},
}
@@ -744,7 +754,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri
// To create drafts, use the CreateEmail function instead.
//
// To delete mails, use the DeleteEmails function instead.
func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, ctx Context) (map[string]*Email, SessionState, State, Language, Error) {
func (j *Client) UpdateEmails(accountId string, updates map[string]PatchObject, ctx Context) (map[string]*Email, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
Update: updates,
@@ -770,6 +780,21 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
})
}
func (j *Client) UpdateEmail(accountId string, id string, changes EmailChange, ctx Context) (Email, SessionState, State, Language, Error) {
return update(j, "UpdateEmail", EmailType,
func(update map[string]PatchObject) EmailSetCommand {
return EmailSetCommand{AccountId: accountId, Update: update}
},
func(id string) EmailGetCommand {
return EmailGetCommand{AccountId: accountId, Ids: []string{id}}
},
func(resp EmailSetResponse) map[string]SetError { return resp.NotUpdated },
func(resp EmailGetResponse) Email { return resp.List[0] },
id, changes,
ctx,
)
}
func (j *Client) DeleteEmails(accountId string, destroyIds []string, ctx Context) (map[string]SetError, SessionState, State, Language, Error) {
return destroy(j, "DeleteEmails", EmailType,
func(accountId string, destroy []string) EmailSetCommand {

View File

@@ -12,6 +12,18 @@ func (r CalendarEventSearchResults) GetPosition() uint { return r.Pos
func (r CalendarEventSearchResults) GetLimit() uint { return r.Limit }
func (r CalendarEventSearchResults) GetTotal() *uint { return r.Total }
func (j *Client) GetCalendarEvents(accountId string, eventIds []string, ctx Context) (CalendarEventGetResponse, SessionState, State, Language, Error) {
return get(j, "GetCalendarEvents", CalendarEventType,
func(accountId string, ids []string) CalendarEventGetCommand {
return CalendarEventGetCommand{AccountId: accountId, Ids: eventIds}
},
CalendarEventGetResponse{},
identity1,
accountId, eventIds,
ctx,
)
}
func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position int, limit uint, calculateTotal bool,
@@ -38,7 +50,16 @@ func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
)
}
type CalendarEventChanges = ChangesTemplate[CalendarEvent]
type CalendarEventChanges ChangesTemplate[CalendarEvent]
var _ Changes[CalendarEvent] = CalendarEventChanges{}
func (c CalendarEventChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c CalendarEventChanges) GetOldState() State { return c.OldState }
func (c CalendarEventChanges) GetNewState() State { return c.NewState }
func (c CalendarEventChanges) GetCreated() []CalendarEvent { return c.Created }
func (c CalendarEventChanges) GetUpdated() []CalendarEvent { return c.Updated }
func (c CalendarEventChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Calendar Events since a given State.
// @api:tags event,changes
@@ -74,9 +95,9 @@ func (j *Client) GetCalendarEventChanges(accountId string, sinceState State, max
)
}
func (j *Client) CreateCalendarEvent(accountId string, event CalendarEvent, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
func (j *Client) CreateCalendarEvent(accountId string, event CalendarEventChange, ctx Context) (*CalendarEvent, SessionState, State, Language, Error) {
return create(j, "CreateCalendarEvent", CalendarEventType,
func(accountId string, create map[string]CalendarEvent) CalendarEventSetCommand {
func(accountId string, create map[string]CalendarEventChange) CalendarEventSetCommand {
return CalendarEventSetCommand{AccountId: accountId, Create: create}
},
func(accountId string, ref string) CalendarEventGetCommand {

View File

@@ -8,23 +8,13 @@ import (
var NS_IDENTITY = ns(JmapMail)
func (j *Client) GetAllIdentities(accountId string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
return getA(j, "GetAllIdentities", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId}
},
IdentityGetResponse{},
accountId, []string{},
ctx,
)
}
func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) ([]Identity, SessionState, State, Language, Error) {
return getA(j, "GetIdentities", IdentityType,
func (j *Client) GetIdentities(accountId string, identityIds []string, ctx Context) (IdentityGetResponse, SessionState, State, Language, Error) {
return get(j, "GetIdentities", IdentityType,
func(accountId string, ids []string) IdentityGetCommand {
return IdentityGetCommand{AccountId: accountId, Ids: ids}
},
IdentityGetResponse{},
identity1,
accountId, identityIds,
ctx,
)
@@ -140,7 +130,16 @@ func (j *Client) DeleteIdentity(accountId string, destroyIds []string, ctx Conte
)
}
type IdentityChanges = ChangesTemplate[Identity]
type IdentityChanges ChangesTemplate[Identity]
var _ Changes[Identity] = IdentityChanges{}
func (c IdentityChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c IdentityChanges) GetOldState() State { return c.OldState }
func (c IdentityChanges) GetNewState() State { return c.NewState }
func (c IdentityChanges) GetCreated() []Identity { return c.Created }
func (c IdentityChanges) GetUpdated() []Identity { return c.Updated }
func (c IdentityChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Email Identities since a given State.
// @api:tags email,changes

View File

@@ -113,7 +113,16 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, roles []string, ct
})
}
type MailboxChanges = ChangesTemplate[Mailbox]
type MailboxChanges ChangesTemplate[Mailbox]
var _ Changes[Mailbox] = MailboxChanges{}
func (c MailboxChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c MailboxChanges) GetOldState() State { return c.OldState }
func (c MailboxChanges) GetNewState() State { return c.NewState }
func (c MailboxChanges) GetCreated() []Mailbox { return c.Created }
func (c MailboxChanges) GetUpdated() []Mailbox { return c.Updated }
func (c MailboxChanges) GetDestroyed() []string { return c.Destroyed }
func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
return MailboxChanges{

View File

@@ -15,7 +15,16 @@ func (j *Client) GetQuotas(accountIds []string, ctx Context) (map[string]QuotaGe
)
}
type QuotaChanges = ChangesTemplate[Quota]
type QuotaChanges ChangesTemplate[Quota]
var _ Changes[Quota] = QuotaChanges{}
func (c QuotaChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c QuotaChanges) GetOldState() State { return c.OldState }
func (c QuotaChanges) GetNewState() State { return c.NewState }
func (c QuotaChanges) GetCreated() []Quota { return c.Created }
func (c QuotaChanges) GetUpdated() []Quota { return c.Updated }
func (c QuotaChanges) GetDestroyed() []string { return c.Destroyed }
// Retrieve the changes in Quotas since a given State.
// @api:tags quota,changes

View File

@@ -47,6 +47,23 @@ type VacationResponseChange struct {
HtmlBody string `json:"htmlBody,omitempty"`
}
var _ Change = VacationResponseChange{}
func (m VacationResponseChange) AsPatch() (PatchObject, error) {
return toPatchObject(m)
}
type VacationResponseChanges ChangesTemplate[VacationResponse]
var _ Changes[VacationResponse] = VacationResponseChanges{}
func (c VacationResponseChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c VacationResponseChanges) GetOldState() State { return c.OldState }
func (c VacationResponseChanges) GetNewState() State { return c.NewState }
func (c VacationResponseChanges) GetCreated() []VacationResponse { return c.Created }
func (c VacationResponseChanges) GetUpdated() []VacationResponse { return c.Updated }
func (c VacationResponseChanges) GetDestroyed() []string { return c.Destroyed }
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponseChange,
ctx Context) (VacationResponse, SessionState, State, Language, Error) {
logger := j.logger("SetVacationResponse", ctx)

View File

@@ -79,3 +79,11 @@ func parseConsts(pkgID string, suffix string, typeName string) (map[string]strin
}
return result, nil
}
func firstKey[K comparable, V any](m map[K]V) (K, bool) {
for k := range m {
return k, true
}
var zero K
return zero, false
}

View File

@@ -422,38 +422,37 @@ func (s *StalwartTest) fillEvents( //NOSONAR
keywords := pickKeywords()
categories := pickCategories()
sequence := 0
sequence := uint(0)
alertId := id()
alertOffset := pickRandom("-PT5M", "-PT10M", "-PT15M")
obj := CalendarEvent{
Id: "",
obj := CalendarEventChange{
CalendarIds: toBoolMapS(calendarId),
IsDraft: isDraft,
Event: jscalendar.Event{
IsDraft: &isDraft,
EventChange: jscalendar.EventChange{
Type: jscalendar.EventType,
Start: jscalendar.LocalDateTime(start),
Duration: jscalendar.Duration(duration),
Status: status,
Object: jscalendar.Object{
CommonObject: jscalendar.CommonObject{
Uid: uid,
ProdId: productName,
Title: title,
Description: description,
DescriptionContentType: descriptionFormat,
Locale: locale,
Color: color,
Duration: ptr(jscalendar.Duration(duration)),
Status: &status,
ObjectChange: jscalendar.ObjectChange{
CommonObjectChange: jscalendar.CommonObjectChange{
Uid: &uid,
ProdId: &productName,
Title: &title,
Description: &description,
DescriptionContentType: &descriptionFormat,
Locale: &locale,
Color: &color,
},
Sequence: uint(sequence),
ShowWithoutTime: false,
FreeBusyStatus: freeBusy,
Privacy: privacy,
Sequence: uintPtr(sequence),
ShowWithoutTime: boolPtr(false),
FreeBusyStatus: &freeBusy,
Privacy: &privacy,
SentBy: organizerEmail,
Participants: participantObjs,
TimeZone: tz,
HideAttendees: false,
TimeZone: &tz,
HideAttendees: boolPtr(false),
ReplyTo: map[jscalendar.ReplyMethod]string{
jscalendar.ReplyMethodImip: "mailto:" + organizerEmail, //NOSONAR
},
@@ -476,8 +475,8 @@ func (s *StalwartTest) fillEvents( //NOSONAR
}
if EnableEventMayInviteFields {
obj.MayInviteSelf = true
obj.MayInviteOthers = true
obj.MayInviteSelf = boolPtr(true)
obj.MayInviteOthers = boolPtr(true)
boxes.mayInvite = true
}
@@ -492,7 +491,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
}
if mainLocationId != "" {
obj.MainLocationId = mainLocationId
obj.MainLocationId = &mainLocationId
}
err = propmap(i%2 == 0, 1, 1, &obj.Links, func(int, string) (jscalendar.Link, error) {

View File

@@ -332,13 +332,13 @@ func (s *StalwartTest) fillContacts( //NOSONAR
nameObj := createName(person)
language := pickLanguage()
card := ContactCard{
card := ContactCardChange{
Type: jscontact.ContactCardType,
Version: "1.0",
Version: ptr(jscontact.JSContactVersion_1_0),
AddressBookIds: toBoolMap([]string{addressbookId}),
ProdId: productName,
Language: language,
Kind: jscontact.ContactCardKindIndividual,
ProdId: &productName,
Language: &language,
Kind: ptr(jscontact.ContactCardKindIndividual),
Name: &nameObj,
}

View File

@@ -56,12 +56,12 @@ func TestEmails(t *testing.T) {
{
{
resp, sessionState, _, _, err := s.client.GetAllIdentities(accountId, ctx)
resp, sessionState, _, _, err := s.client.GetIdentities(accountId, []string{}, ctx)
require.NoError(err)
require.Equal(session.State, sessionState)
require.Len(resp, 1)
require.Equal(user.email, resp[0].Email)
require.Equal(user.description, resp[0].Name)
require.Len(resp.List, 1)
require.Equal(user.email, resp.List[0].Email)
require.Equal(user.description, resp.List[0].Name)
}
{
@@ -188,13 +188,13 @@ func TestSendingEmails(t *testing.T) {
{
var identity Identity
{
identities, _, _, _, err := s.client.GetAllIdentities(accountId, ctx)
resp, _, _, _, err := s.client.GetIdentities(accountId, []string{}, ctx)
require.NoError(err)
require.NotEmpty(identities)
identity = identities[0]
require.NotEmpty(resp.List)
identity = resp.List[0]
}
create := EmailCreate{
create := EmailChange{
Keywords: toBoolMapS("test"),
Subject: subject,
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
@@ -214,7 +214,7 @@ func TestSendingEmails(t *testing.T) {
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
}
update := EmailCreate{
update := EmailChange{
From: []EmailAddress{{Name: fromName, Email: from.email}},
To: []EmailAddress{{Name: to.description, Email: to.email}},
Cc: []EmailAddress{{Name: cc.description, Email: cc.email}},
@@ -240,7 +240,7 @@ func TestSendingEmails(t *testing.T) {
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
require.Equal(notFound[0], created.Id)
var ok bool
updatedMailboxId, ok = structs.FirstKey(email.MailboxIds)
updatedMailboxId, ok = firstKey(email.MailboxIds)
require.True(ok)
}

View File

@@ -2,6 +2,7 @@ package jmap
import (
"encoding/json"
"fmt"
"io"
"time"
@@ -1326,12 +1327,17 @@ type JmapCommand interface {
GetObjectType() ObjectType
}
type JmapResponse[T Foo] interface {
GetMarker() T
}
type GetCommand[T Foo] interface {
JmapCommand
GetResponse() GetResponse[T]
}
type GetResponse[T Foo] interface {
JmapResponse[T]
GetState() State
GetNotFound() []string
GetList() []T
@@ -1343,12 +1349,12 @@ type SetCommand[T Foo] interface {
}
type SetResponse[T Foo] interface {
JmapResponse[T]
GetNotCreated() map[string]SetError
GetNotUpdated() map[string]SetError
GetNotDestroyed() map[string]SetError
GetOldState() State
GetNewState() State
GetMarker() T
}
type Change interface {
@@ -1361,13 +1367,13 @@ type ChangesCommand[T Foo] interface {
}
type ChangesResponse[T Foo] interface {
JmapResponse[T]
GetOldState() State
GetNewState() State
GetHasMoreChanges() bool
GetCreated() []string
GetUpdated() []string
GetDestroyed() []string
GetMarker() T
}
type QueryCommand[T Foo] interface {
@@ -1376,8 +1382,8 @@ type QueryCommand[T Foo] interface {
}
type QueryResponse[T Foo] interface {
JmapResponse[T]
GetQueryState() State
GetMarker() T
}
type UploadCommand[T Foo] interface {
@@ -1386,7 +1392,7 @@ type UploadCommand[T Foo] interface {
}
type UploadResponse[T Foo] interface {
GetMarker() T
JmapResponse[T]
}
type ParseCommand[T Foo] interface {
@@ -1395,7 +1401,7 @@ type ParseCommand[T Foo] interface {
}
type ParseResponse[T Foo] interface {
GetMarker() T
JmapResponse[T]
}
type ChangesTemplate[T Foo] struct {
@@ -1407,6 +1413,15 @@ type ChangesTemplate[T Foo] struct {
Destroyed []string `json:"destroyed,omitempty"`
}
type Changes[T Foo] interface {
GetHasMoreChanges() bool
GetOldState() State
GetNewState() State
GetCreated() []T
GetUpdated() []T
GetDestroyed() []string
}
type SearchResultsTemplate[T Foo] struct {
Results []T `json:"results"`
CanCalculateChanges bool `json:"canCalculateChanges"`
@@ -2965,6 +2980,7 @@ type EmailSubmissionGetResponse struct {
var _ GetResponse[EmailSubmission] = &EmailSubmissionGetResponse{}
func (r EmailSubmissionGetResponse) GetMarker() EmailSubmission { return EmailSubmission{} }
func (r EmailSubmissionGetResponse) GetState() State { return r.State }
func (r EmailSubmissionGetResponse) GetNotFound() []string { return r.NotFound }
func (r EmailSubmissionGetResponse) GetList() []EmailSubmission { return r.List }
@@ -3268,6 +3284,7 @@ type MailboxGetResponse struct {
var _ GetResponse[Mailbox] = &MailboxGetResponse{}
func (r MailboxGetResponse) GetMarker() Mailbox { return Mailbox{} }
func (r MailboxGetResponse) GetState() State { return r.State }
func (r MailboxGetResponse) GetNotFound() []string { return r.NotFound }
func (r MailboxGetResponse) GetList() []Mailbox { return r.List }
@@ -3396,7 +3413,7 @@ var _ QueryResponse[Mailbox] = &MailboxQueryResponse{}
func (r MailboxQueryResponse) GetQueryState() State { return r.QueryState }
func (r MailboxQueryResponse) GetMarker() Mailbox { return Mailbox{} }
type EmailCreate struct {
type EmailChange struct {
// The set of Mailbox ids this Email belongs to.
//
// An Email in the mail store MUST belong to one or more Mailboxes at all times
@@ -3498,7 +3515,11 @@ type EmailCreate struct {
Attachments []EmailBodyPart `json:"attachments,omitempty"`
}
type EmailUpdate map[string]any
var _ Change = EmailChange{}
func (e EmailChange) AsPatch() (PatchObject, error) {
return toPatchObject(e)
}
type EmailSetCommand struct {
// The id of the account to use.
@@ -3520,7 +3541,7 @@ type EmailSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
Create map[string]EmailCreate `json:"create,omitempty"`
Create map[string]EmailChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
@@ -3547,7 +3568,7 @@ type EmailSetCommand struct {
//
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
// processes it the same either way.
Update map[string]EmailUpdate `json:"update,omitempty"`
Update map[string]PatchObject `json:"update,omitempty"`
// A list of ids for Email objects to permanently delete, or null if no objects are to be destroyed.
Destroy []string `json:"destroy,omitempty"`
@@ -3966,6 +3987,7 @@ type IdentityGetResponse struct {
var _ GetResponse[Identity] = &IdentityGetResponse{}
func (r IdentityGetResponse) GetMarker() Identity { return Identity{} }
func (r IdentityGetResponse) GetState() State { return r.State }
func (r IdentityGetResponse) GetNotFound() []string { return r.NotFound }
func (r IdentityGetResponse) GetList() []Identity { return r.List }
@@ -4050,6 +4072,7 @@ type VacationResponseGetResponse struct {
var _ GetResponse[VacationResponse] = &VacationResponseGetResponse{}
func (r VacationResponseGetResponse) GetMarker() VacationResponse { return VacationResponse{} }
func (r VacationResponseGetResponse) GetState() State { return r.State }
func (r VacationResponseGetResponse) GetNotFound() []string { return r.NotFound }
func (r VacationResponseGetResponse) GetList() []VacationResponse { return r.List }
@@ -4195,6 +4218,26 @@ var _ Idable = &Blob{}
func (f Blob) GetObjectType() ObjectType { return BlobType }
func (f Blob) GetId() string { return f.Id }
type BlobChange struct {
}
var _ Change = BlobChange{}
func (m BlobChange) AsPatch() (PatchObject, error) {
return nil, fmt.Errorf("BlobChange is unsupported")
}
type BlobChanges ChangesTemplate[Blob]
var _ Changes[Blob] = BlobChanges{}
func (c BlobChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
func (c BlobChanges) GetOldState() State { return c.OldState }
func (c BlobChanges) GetNewState() State { return c.NewState }
func (c BlobChanges) GetCreated() []Blob { return c.Created }
func (c BlobChanges) GetUpdated() []Blob { return c.Updated }
func (c BlobChanges) GetDestroyed() []string { return c.Destroyed }
type BlobGetCommand struct {
AccountId string `json:"accountId"`
Ids []string `json:"ids,omitempty"`
@@ -4255,6 +4298,7 @@ type BlobGetResponse struct {
var _ GetResponse[Blob] = &BlobGetResponse{}
func (r BlobGetResponse) GetMarker() Blob { return Blob{} }
func (r BlobGetResponse) GetState() State { return r.State }
func (r BlobGetResponse) GetNotFound() []string { return r.NotFound }
func (r BlobGetResponse) GetList() []Blob { return r.List }
@@ -6370,6 +6414,15 @@ var _ Idable = &Quota{}
func (f Quota) GetObjectType() ObjectType { return QuotaType }
func (f Quota) GetId() string { return f.Id }
type QuotaChange struct {
}
var _ Change = QuotaChange{}
func (m QuotaChange) AsPatch() (PatchObject, error) {
return nil, fmt.Errorf("QuotaChange is unsupported")
}
// See [RFC8098] for the exact meaning of these different fields.
//
// These fields are defined as case insensitive in [RFC8098] but are case sensitive in this RFC
@@ -6492,6 +6545,7 @@ type QuotaGetResponse struct {
var _ GetResponse[Quota] = &QuotaGetResponse{}
func (r QuotaGetResponse) GetMarker() Quota { return Quota{} }
func (r QuotaGetResponse) GetState() State { return r.State }
func (r QuotaGetResponse) GetNotFound() []string { return r.NotFound }
func (r QuotaGetResponse) GetList() []Quota { return r.List }
@@ -6593,6 +6647,7 @@ type AddressBookGetResponse struct {
var _ GetResponse[AddressBook] = &AddressBookGetResponse{}
func (r AddressBookGetResponse) GetMarker() AddressBook { return AddressBook{} }
func (r AddressBookGetResponse) GetState() State { return r.State }
func (r AddressBookGetResponse) GetNotFound() []string { return r.NotFound }
func (r AddressBookGetResponse) GetList() []AddressBook { return r.List }
@@ -7168,6 +7223,7 @@ type ContactCardGetResponse struct {
var _ GetResponse[ContactCard] = &ContactCardGetResponse{}
func (r ContactCardGetResponse) GetMarker() ContactCard { return ContactCard{} }
func (r ContactCardGetResponse) GetState() State { return r.State }
func (r ContactCardGetResponse) GetNotFound() []string { return r.NotFound }
func (r ContactCardGetResponse) GetList() []ContactCard { return r.List }
@@ -7251,7 +7307,7 @@ type ContactCardSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
Create map[string]ContactCard `json:"create,omitempty"`
Create map[string]ContactCardChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
@@ -7420,6 +7476,7 @@ type CalendarGetResponse struct {
var _ GetResponse[Calendar] = &CalendarGetResponse{}
func (r CalendarGetResponse) GetMarker() Calendar { return Calendar{} }
func (r CalendarGetResponse) GetState() State { return r.State }
func (r CalendarGetResponse) GetNotFound() []string { return r.NotFound }
func (r CalendarGetResponse) GetList() []Calendar { return r.List }
@@ -7896,6 +7953,7 @@ type CalendarEventGetResponse struct {
var _ GetResponse[CalendarEvent] = &CalendarEventGetResponse{}
func (r CalendarEventGetResponse) GetMarker() CalendarEvent { return CalendarEvent{} }
func (r CalendarEventGetResponse) GetState() State { return r.State }
func (r CalendarEventGetResponse) GetNotFound() []string { return r.NotFound }
func (r CalendarEventGetResponse) GetList() []CalendarEvent { return r.List }
@@ -7985,7 +8043,7 @@ type CalendarEventSetCommand struct {
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
Create map[string]CalendarEvent `json:"create,omitempty"`
Create map[string]CalendarEventChange `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.

View File

@@ -2058,8 +2058,8 @@ func (e Exemplar) EmailChanges() EmailChanges {
}
}
func (e Exemplar) Changes() (Changes, string, string) {
return Changes{
func (e Exemplar) Changes() (ObjectChanges, string, string) {
return ObjectChanges{
MaxChanges: 3,
Mailboxes: &MailboxChangesResponse{
AccountId: e.AccountId,

View File

@@ -35,14 +35,6 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSON
})
}
func getA[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T]]( //NOSONAR
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,
resp GETRESP,
accountId string, ids []string, ctx Context) ([]T, SessionState, State, Language, Error) {
return get(client, name, objType, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, ids, ctx)
}
func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, objType ObjectType,
getCommandFactory func(string, []string) GETREQ,

View File

@@ -50,6 +50,11 @@ func mcid(accountId string, tag string) string {
return accountId + ":" + tag
}
func bail[R JmapResponse[T], T Foo](err Error) (R, SessionState, State, Language, Error) {
var zero R
return zero, EmptySessionState, EmptyState, NoLanguage, err
}
type Cmdr interface {
ApiSupplier
Hooks