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 4bee5f01f5
commit 2883f04488
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

View File

@@ -10,14 +10,14 @@ import (
)
// Get attributes of a given account.
func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) GetAccountById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, account, err := req.GetAccountForMail()
if err != nil {
return req.error(accountId, err)
}
var body jmap.Account = account
return req.respond(accountId, body, req.session.State, AccountResponseObjectType, "")
return req.respond(accountId, body, req.session.State, AccountResponseObjectType, jmap.EmptyState, jmap.NoLanguage)
})
}
@@ -36,7 +36,7 @@ func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) {
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithId) int { return strings.Compare(a.AccountId, b.AccountId) })
var RBODY []AccountWithId = list
return req.respondN(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, "")
return req.respondN(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.EmptyState, jmap.NoLanguage)
})
}
@@ -66,7 +66,7 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithIdAndIdentities) int { return strings.Compare(a.AccountId, b.AccountId) })
var RBODY []AccountWithIdAndIdentities = list
return req.respondN(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state)
return req.respondN(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang)
})
}
@@ -76,7 +76,7 @@ type AccountWithId struct {
}
type AccountWithIdAndIdentities struct {
AccountId string `json:"accountId,omitempty"`
jmap.Account
AccountId string `json:"accountId,omitempty"`
Identities []jmap.Identity `json:"identities,omitempty"`
jmap.Account
}

View File

@@ -2,188 +2,32 @@ package groupware
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Get all addressbooks of an account.
func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, []string{}, req.ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.AddressBookGetResponse = addressbooks
return req.respond(accountId, body, sessionState, AddressBookResponseObjectType, state)
})
getall(AddressBook, w, r, g, g.jmap.GetAddressbooks)
}
// Get an addressbook of an account by its identifier.
func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
logger := log.From(l)
addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, []string{addressBookId}, req.ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
switch len(addressbooks.List) {
case 0:
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
case 1:
return req.respond(accountId, addressbooks.List[0], sessionState, ContactResponseObjectType, state)
default:
logger.Error().Msgf("found %d addressbooks matching '%s' instead of 1", len(addressbooks.List), addressBookId)
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
}
})
func (g *Groupware) GetAddressbookById(w http.ResponseWriter, r *http.Request) {
get(AddressBook, w, r, g, g.jmap.GetAddressbooks)
}
// Get the changes to Address Books since a certain State.
// @api:tags addressbook,changes
func (g *Groupware) GetAddressBookChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list addressbook changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetAddressbookChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, AddressBookResponseObjectType, state)
})
changes(AddressBook, w, r, g, g.jmap.GetAddressbookChanges)
}
func (g *Groupware) CreateAddressBook(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
var create jmap.AddressBookChange
err := req.bodydoc(&create, "The address book to create")
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
created, sessionState, state, lang, jerr := g.jmap.CreateAddressBook(accountId, create, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
})
create(AddressBook, w, r, g, nil, g.jmap.CreateAddressBook)
}
func (g *Groupware) DeleteAddressBook(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteAddressBook(accountId, []string{addressBookId}, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range deleted {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteAddressBook,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteAddressBook,
))
}
}
return req.noContent(accountId, sessionState, AddressBookResponseObjectType, state)
})
delete(AddressBook, w, r, g, g.jmap.DeleteAddressBook)
}
func (g *Groupware) ModifyAddressBook(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(UriParamAddressBookId, "The unique identifier of the AddressBook to modify")
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamAddressBookId, log.SafeString(id))
var change jmap.AddressBookChange
err = req.body(&change)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := g.jmap.UpdateAddressBook(accountId, id, change, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, AddressBookResponseObjectType, state)
})
modify(AddressBook, w, r, g, g.jmap.UpdateAddressBook)
}

View File

@@ -14,31 +14,7 @@ const (
)
func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForBlob()
if err != nil {
return req.error(accountId, err)
}
l := req.logger.With().Str(logAccountId, accountId)
blobId, err := req.PathParam(UriParamBlobId)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamBlobId, blobId)
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
res, sessionState, state, lang, jerr := g.jmap.GetBlobMetadata(accountId, blobId, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if res == nil {
return req.notFound(accountId, sessionState, BlobResponseObjectType, state)
}
return req.respond(accountId, res, sessionState, BlobResponseObjectType, state)
})
get(Blob, w, r, g, g.jmap.GetBlobMetadata)
}
func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {

View File

@@ -2,187 +2,32 @@ package groupware
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Get all calendars of an account.
func (g *Groupware) GetCalendars(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
calendars, sessionState, state, lang, jerr := g.jmap.GetCalendars(accountId, nil, req.ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, calendars, sessionState, CalendarResponseObjectType, state)
})
getall(Calendar, w, r, g, g.jmap.GetCalendars)
}
// Get a calendar of an account by its identifier.
func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
calendarId, err := req.PathParam(UriParamCalendarId)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
logger := log.From(l)
calendars, sessionState, state, lang, jerr := g.jmap.GetCalendars(accountId, single(calendarId), req.ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
switch len(calendars.List) {
case 0:
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
case 1:
return req.respond(accountId, calendars.List[0], sessionState, ContactResponseObjectType, state)
default:
logger.Error().Msgf("found %d calendars matching '%s' instead of 1", len(calendars.List), calendarId)
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
}
})
get(Calendar, w, r, g, g.jmap.GetCalendars)
}
// Get the changes to Calendars since a certain State.
// @api:tags calendar,changes
func (g *Groupware) GetCalendarChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list calendar changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetCalendarChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, CalendarResponseObjectType, state)
})
changes(Calendar, w, r, g, g.jmap.GetCalendarChanges)
}
func (g *Groupware) CreateCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
var create jmap.CalendarChange
err := req.bodydoc(&create, "The calendar to create")
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
created, sessionState, state, lang, jerr := g.jmap.CreateCalendar(accountId, create, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
})
create(Calendar, w, r, g, nil, g.jmap.CreateCalendar)
}
func (g *Groupware) DeleteCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
calendarId, err := req.PathParam(UriParamCalendarId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamCalendarId, log.SafeString(calendarId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteCalendar(accountId, single(calendarId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range deleted {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteCalendar,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteCalendar,
))
}
}
return req.noContent(accountId, sessionState, CalendarResponseObjectType, state)
})
delete(Calendar, w, r, g, g.jmap.DeleteCalendar)
}
func (g *Groupware) ModifyCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(UriParamCalendarId, "The unique identifier of the Calendar to modify")
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamCalendarId, log.SafeString(id))
var change jmap.CalendarChange
err = req.body(&change)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendar(accountId, id, change, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, CalendarResponseObjectType, state)
})
modify(Calendar, w, r, g, g.jmap.UpdateCalendar)
}

View File

@@ -82,8 +82,8 @@ func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSON
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.Changes = changes
var body jmap.ObjectChanges = changes
return req.respond(accountId, body, sessionState, "", state)
return req.respond(accountId, body, sessionState, "", state, lang)
})
}

View File

@@ -91,7 +91,7 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
}
if contacts, ok := contactsByAccountId[accountId]; ok {
return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state)
return req.respondN(accountIds, contacts, sessionState, ContactResponseObjectType, state, lang)
} else {
return req.notFoundN(accountIds, sessionState, ContactResponseObjectType, state)
}
@@ -99,192 +99,36 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
}
func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
contactId, err := req.PathParam(UriParamContactId)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, single(contactId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
switch len(contacts.List) {
case 0:
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
case 1:
return req.respond(accountId, contacts.List[0], sessionState, ContactResponseObjectType, state)
default:
logger.Error().Msgf("found %d contacts matching '%s' instead of 1", len(contacts.List), contactId)
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
}
})
get(Contact, w, r, g, g.jmap.GetContactCards)
}
func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, []string{}, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body []jmap.ContactCard = contacts.List
return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
})
getallpaged(Contact, w, r, g,
g.jmap.GetContactCards,
func(cid string) jmap.ContactCardFilterElement {
return jmap.ContactCardFilterCondition{InAddressBook: cid}
},
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyUpdated, IsAscending: true}},
curryMapQuery(g.jmap.QueryContactCards),
)
}
// Get changes to Contacts since a given State
// @api:tags contact,changes
func (g *Groupware) GetContactsChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
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 contact changes"))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetContactCardChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.ContactCardChanges = changes
return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
})
changes(Contact, w, r, g, g.jmap.GetContactCardChanges)
}
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With()
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
var create jmap.ContactCard
err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
created, sessionState, state, lang, jerr := g.jmap.CreateContactCard(accountId, create, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, ContactResponseObjectType, state)
})
create(Contact, w, r, g, nil, g.jmap.CreateContactCard)
}
func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
contactId, err := req.PathParam(UriParamContactId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteContactCard(accountId, single(contactId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range deleted {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteContact,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteContact,
))
}
}
return req.noContent(accountId, sessionState, ContactResponseObjectType, state)
})
delete(Contact, w, r, g, g.jmap.DeleteContactCard)
}
func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needContactWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(UriParamContactId, "The unique identifier of the Contact to modify")
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamContactId, log.SafeString(id))
var change jmap.ContactCardChange
err = req.body(&change)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := g.jmap.UpdateContactCard(accountId, id, change, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, ContactResponseObjectType, state)
})
modify(Contact, w, r, g, g.jmap.UpdateContactCard)
}
func mapContactCardSort(s SortCrit) jmap.ContactCardComparator {

View File

@@ -24,37 +24,8 @@ import (
// Get the changes tp Emails since a certain State.
// @api:tags email,changes
func (g *Groupware) GetEmailChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.EmptyState
if s := req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list email changes"); s != "" {
l = l.Str(HeaderParamSince, log.SafeString(s))
sinceState = jmap.State(s)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetEmailChanges(accountId, sinceState, true, g.config.maxBodyValueBytes, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state)
changes(Email, w, r, g, func(accountId string, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.EmailChanges, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return g.jmap.GetEmailChanges(accountId, sinceState, true, g.config.maxBodyValueBytes, maxChanges, ctx)
})
}
@@ -67,63 +38,31 @@ func (g *Groupware) GetEmailChanges(w http.ResponseWriter, r *http.Request) {
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
// respectively.
func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) { //NOSONAR
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
collapseThreads := false
fetchBodies := false
withThreads := true
query(Email, w, r, g, g.defaults.emailLimit,
func(req Request, accountId, containerId string, offset int, limit uint, ctx jmap.Context) (jmap.EmailSearchResults, jmap.SessionState, jmap.State, jmap.Language, *Error) {
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, containerId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
if jerr != nil {
return emails, sessionState, state, lang, req.apiErrorFromJmap(req.observeJmapError(jerr))
}
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
sanitized, err := req.sanitizeEmails(emails.Results)
if err != nil {
return emails, sessionState, state, lang, err
}
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return req.error(accountId, err)
}
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Int(QueryParamOffset, offset)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamLimit, limit)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
collapseThreads := false
fetchBodies := false
withThreads := true
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
sanitized, err := req.sanitizeEmails(emails.Results)
if err != nil {
return req.error(accountId, err)
}
safe := jmap.EmailSearchResults{
Results: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Position: emails.Position,
CanCalculateChanges: emails.CanCalculateChanges,
}
return req.respond(accountId, safe, sessionState, EmailResponseObjectType, state)
})
safe := jmap.EmailSearchResults{
Results: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Position: emails.Position,
CanCalculateChanges: emails.CanCalculateChanges,
}
return safe, sessionState, state, lang, nil
},
)
}
func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NOSONAR
@@ -196,7 +135,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
}
if len(ids) == 1 {
logger := log.From(l.Str("id", log.SafeString(id)))
logger := log.From(l.Str(UriParamEmailId, log.SafeString(id)))
ctx := req.ctx.WithLogger(logger)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, ids, true, g.config.maxBodyValueBytes, markAsSeen, true, ctx)
if jerr != nil {
@@ -209,10 +148,10 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
if err != nil {
return req.error(accountId, err)
}
return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state, lang)
}
} else {
logger := log.From(l.Array("ids", log.SafeStringArray(ids)))
logger := log.From(l.Array(UriParamEmailId, log.SafeStringArray(ids)))
ctx := req.ctx.WithLogger(logger)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, ids, true, g.config.maxBodyValueBytes, markAsSeen, false, ctx)
if jerr != nil {
@@ -225,7 +164,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO
if err != nil {
return req.error(accountId, err)
}
return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, sanitized, sessionState, EmailResponseObjectType, state, lang)
}
}
})
@@ -284,7 +223,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
return req.error(accountId, err)
}
var body []jmap.EmailBodyPart = email.Attachments
return req.respond(accountId, body, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, body, sessionState, EmailResponseObjectType, state, lang)
})
} else {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
@@ -399,7 +338,7 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, changes, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -428,7 +367,8 @@ type SnippetWithoutEmailId struct {
type EmailWithSnippetsSearchResults struct {
Results []EmailWithSnippets `json:"results"`
Total uint `json:"total,omitzero"`
Total *uint `json:"total,omitzero"`
Position uint `json:"position"`
Limit uint `json:"limit,omitzero"`
QueryState jmap.State `json:"queryState,omitempty"`
}
@@ -610,20 +550,32 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA
return req.error(accountId, err)
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return req.error(accountId, err)
}
logger = log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)))
ctx := req.ctx.WithLogger(logger)
if !filter.IsNotEmpty() {
filter = nil
}
fetchBodies := false
calculateTotal := true
if b, ok, err := req.parseBoolParam(QueryParamCalculateTotal, true); err != nil {
return req.error(accountId, err)
} else if ok {
calculateTotal = b
l = l.Bool(QueryParamCalculateTotal, calculateTotal)
}
resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, offset, limit, fetchBodies, g.config.maxBodyValueBytes, ctx)
fetchBodies := false
collapseThreads := false
logger = log.From(l)
ctx := req.ctx.WithLogger(logger)
resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(single(accountId), filter, offset, limit, collapseThreads, calculateTotal, fetchBodies, g.config.maxBodyValueBytes, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
@@ -653,12 +605,18 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA
}
}
var total *uint = nil
if calculateTotal {
total = &results.Total
}
return req.respond(accountId, EmailWithSnippetsSearchResults{
Results: flattened,
Total: results.Total,
Total: total,
Position: results.Position,
Limit: results.Limit,
QueryState: results.QueryState,
}, sessionState, EmailResponseObjectType, state)
}, sessionState, EmailResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, EmailResponseObjectType, state)
}
@@ -720,7 +678,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
QueryState: state,
}
return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state)
return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
} else {
withThreads := true
@@ -759,7 +717,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
QueryState: state,
}
return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state)
return req.respondN(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -822,127 +780,53 @@ func findSentMailboxId(j *jmap.Client, accountId string, req Request, ctx jmap.C
}
func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
logger := req.logger
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return req.error(accountId, gwerr)
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
ctx := req.ctx.WithLogger(logger)
var body jmap.EmailCreate
err := req.body(&body)
if err != nil {
return req.error(accountId, err)
}
if len(body.MailboxIds) < 1 {
mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, req, ctx)
if mailboxId != "" {
body.MailboxIds[mailboxId] = true
} else {
return resp
create(Email, w, r, g,
func(r Request, accountId string, body *jmap.EmailChange, ctx jmap.Context) (bool, Response) {
if len(body.MailboxIds) < 1 {
mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, r, ctx)
if mailboxId != "" {
body.MailboxIds[mailboxId] = true
} else {
return false, resp
}
}
}
created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, "", ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, EmailResponseObjectType, state)
})
return true, Response{}
},
func(accountId string, body jmap.EmailChange, ctx jmap.Context) (*jmap.Email, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return g.jmap.CreateEmail(accountId, body, "", ctx)
},
)
}
func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
logger := req.logger
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return req.error(accountId, gwerr)
}
replaceId, err := req.PathParam(UriParamEmailId)
if err != nil {
return req.error(accountId, err)
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
ctx := req.ctx.WithLogger(logger)
var body jmap.EmailCreate
err = req.body(&body)
if err != nil {
return req.error(accountId, err)
}
if len(body.MailboxIds) < 1 {
mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, req, ctx)
if mailboxId != "" {
body.MailboxIds[mailboxId] = true
} else {
return resp
replaceId := ""
create(Email, w, r, g,
func(r Request, accountId string, body *jmap.EmailChange, ctx jmap.Context) (bool, Response) {
if len(body.MailboxIds) < 1 {
mailboxId, resp := findDraftsMailboxId(g.jmap, accountId, r, ctx)
if mailboxId != "" {
body.MailboxIds[mailboxId] = true
} else {
return false, resp
}
}
var err *Error
replaceId, err = r.PathParam(UriParamEmailId)
if err != nil {
return false, r.error(accountId, err)
}
}
created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, replaceId, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, EmailResponseObjectType, state)
})
return true, Response{}
},
func(accountId string, body jmap.EmailChange, ctx jmap.Context) (*jmap.Email, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
ctx = ctx.WithLogger(log.From(ctx.Logger.With().Str("replaceId", replaceId)))
return g.jmap.CreateEmail(accountId, body, replaceId, ctx)
},
)
}
func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return req.error(accountId, gwerr)
}
l.Str(logAccountId, accountId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
var body map[string]any
err = req.body(&body)
if err != nil {
return req.error(accountId, err)
}
updates := map[string]jmap.EmailUpdate{
emailId: body,
}
result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, updates, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if result == nil {
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR
}
updatedEmail, ok := result[emailId]
if !ok {
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR
}
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
})
modify(Email, w, r, g, g.jmap.UpdateEmail)
}
type emailKeywordUpdates struct {
@@ -986,14 +870,14 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
return req.noop(accountId)
}
patch := jmap.EmailUpdate{}
patch := jmap.PatchObject{}
for _, keyword := range body.Add {
patch["keywords/"+keyword] = true //NOSONAR
}
for _, keyword := range body.Remove {
patch["keywords/"+keyword] = nil //NOSONAR
}
patches := map[string]jmap.EmailUpdate{
patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1003,16 +887,16 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
}
if result == nil {
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response",
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")))
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR
}
updatedEmail, ok := result[emailId]
if !ok {
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID",
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR
}
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1048,11 +932,11 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { /
return req.noop(accountId)
}
patch := jmap.EmailUpdate{}
patch := jmap.PatchObject{}
for _, keyword := range body {
patch["keywords/"+keyword] = true
}
patches := map[string]jmap.EmailUpdate{
patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1074,7 +958,7 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { /
if updatedEmail == nil {
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
} else {
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -1111,11 +995,11 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
return req.noop(accountId)
}
patch := jmap.EmailUpdate{}
patch := jmap.PatchObject{}
for _, keyword := range body {
patch["keywords/"+keyword] = nil
}
patches := map[string]jmap.EmailUpdate{
patches := map[string]jmap.PatchObject{
emailId: patch,
}
@@ -1137,52 +1021,14 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
if updatedEmail == nil {
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
} else {
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state, lang)
}
})
}
// Delete an email by its unique identifier.
func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return req.error(accountId, gwerr)
}
l.Str(logAccountId, log.SafeString(accountId))
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, single(emailId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range resp {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteEmail,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteEmail,
))
}
}
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
})
delete(Email, w, r, g, g.jmap.DeleteEmails)
}
// Delete a set of emails by their unique identifiers.
@@ -1190,44 +1036,7 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
// The identifiers of the emails to delete are specified as part of the request
// body, as an array of strings.
func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
/// @api body
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return req.error(accountId, gwerr)
}
l.Str(logAccountId, accountId)
var emailIds []string
err := req.body(&emailIds)
if err != nil {
return req.error(accountId, err)
}
l.Array("emailIds", log.SafeStringArray(emailIds))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
resp, sessionState, state, lang, jerr := g.jmap.DeleteEmails(accountId, emailIds, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if len(resp) > 0 {
meta := make(map[string]any, len(resp))
for emailId, e := range resp {
meta[emailId] = e.Description
}
return req.error(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteEmail,
withMeta(meta),
))
}
return req.noContent(accountId, sessionState, EmailResponseObjectType, state)
})
deleteMany(Email, w, r, g, g.jmap.DeleteEmails)
}
func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONAR
@@ -1285,7 +1094,7 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONA
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, resp, sessionState, EmailResponseObjectType, state)
return req.respond(accountId, resp, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1456,7 +1265,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { //N
return req.respond(accountId, AboutEmailResponse{
Email: sanitized,
RequestId: reqId,
}, sessionState, EmailResponseObjectType, state)
}, sessionState, EmailResponseObjectType, state, lang)
})
}
@@ -1733,7 +1542,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
Total: total,
Limit: limit,
Offset: offset,
}, sessionState, EmailResponseObjectType, state)
}, sessionState, EmailResponseObjectType, state, lang)
})
}

View File

@@ -43,7 +43,7 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
filter := jmap.CalendarEventFilterCondition{
InCalendar: calendarId,
}
sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyUpdated, IsAscending: false}}
sortBy := []jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: false}}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
@@ -53,139 +53,55 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
}
if events, ok := eventsByAccountId[accountId]; ok {
return req.respond(accountId, events, sessionState, EventResponseObjectType, state)
return req.respond(accountId, events, sessionState, EventResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, EventResponseObjectType, state)
}
})
}
// Get changes to Contacts since a given State
//func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
func curryMapQuery[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any](
f func(accountIds []string, filter FILTER, sortBy []COMP, position int, limit uint, calculateTotal bool, ctx jmap.Context) (map[string]SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
m, sessionState, state, lang, err := f(single(accountId), filter, sortBy, offset, limit, true, ctx)
return m[accountId], sessionState, state, lang, err
}
}
func (g *Groupware) GetAllEvents(w http.ResponseWriter, r *http.Request) {
getallpaged(Event, w, r, g,
g.jmap.GetCalendarEvents,
func(cid string) jmap.CalendarEventFilterElement {
return jmap.CalendarEventFilterCondition{InCalendar: cid}
},
[]jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}},
curryMapQuery(g.jmap.QueryCalendarEvents),
)
}
func (g *Groupware) GetEventById(w http.ResponseWriter, r *http.Request) {
get(Event, w, r, g, g.jmap.GetCalendarEvents)
}
// Get changes to Calendar Events since a given State
// @api:tags event,changes
func (g *Groupware) GetEventChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
var maxChanges uint = 0
if v, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0); err != nil {
return req.error(accountId, err)
} else if ok {
maxChanges = v
l = l.Uint(QueryParamMaxChanges, v)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list event changes"))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetCalendarEventChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.CalendarEventChanges = changes
return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
})
changes(Event, w, r, g, g.jmap.GetCalendarEventChanges)
}
func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With()
var create jmap.CalendarEvent
err := req.body(&create)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
created, sessionState, state, lang, jerr := g.jmap.CreateCalendarEvent(accountId, create, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, EventResponseObjectType, state)
})
func (g *Groupware) CreateEvent(w http.ResponseWriter, r *http.Request) {
create(Event, w, r, g, nil, g.jmap.CreateCalendarEvent)
}
func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
eventId, err := req.PathParam(UriParamEventId)
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamEventId, log.SafeString(eventId))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteCalendarEvent(accountId, single(eventId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range deleted {
desc := e.Description
if desc != "" {
return req.errorS(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteContact,
withDetail(e.Description),
), sessionState)
} else {
return req.errorS(accountId, apiError(
req.errorId(),
ErrorFailedToDeleteContact,
), sessionState)
}
}
return req.noContent(accountId, sessionState, EventResponseObjectType, state)
})
func (g *Groupware) DeleteEvent(w http.ResponseWriter, r *http.Request) {
delete(Event, w, r, g, g.jmap.DeleteCalendarEvent)
}
func (g *Groupware) ModifyCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(UriParamEventId, "The unique identifier of the Calendar Event to modify")
if err != nil {
return req.error(accountId, err)
}
l.Str(UriParamEventId, log.SafeString(id))
var change jmap.CalendarEventChange
err = req.body(&change)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendarEvent(accountId, id, change, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, EventResponseObjectType, state)
})
func (g *Groupware) ModifyEvent(w http.ResponseWriter, r *http.Request) {
modify(Event, w, r, g, g.jmap.UpdateCalendarEvent)
}
// Parse a blob that contains an iCal file and return it as JSCalendar.
@@ -211,6 +127,6 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, resp, sessionState, EventResponseObjectType, state)
return req.respond(accountId, resp, sessionState, EventResponseObjectType, state, lang)
})
}

View File

@@ -1,170 +1,33 @@
package groupware
import (
"fmt"
"net/http"
"strings"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Get the list of identities that are associated with an account.
func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
ctx := req.ctx.WithLogger(logger)
res, sessionState, state, lang, jerr := g.jmap.GetAllIdentities(accountId, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, res, sessionState, IdentityResponseObjectType, state)
})
getall(Identity, w, r, g, g.jmap.GetIdentities)
}
func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
id, err := req.PathParam(UriParamIdentityId)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
ctx := req.ctx.WithLogger(logger)
res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, single(id), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if len(res) < 1 {
return req.notFound(accountId, sessionState, IdentityResponseObjectType, state)
}
var body jmap.Identity = res[0]
return req.respond(accountId, body, sessionState, IdentityResponseObjectType, state)
})
get(Identity, w, r, g, g.jmap.GetIdentities)
}
func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
ctx := req.ctx.WithLogger(logger)
var identity jmap.IdentityChange
err = req.body(&identity)
if err != nil {
return req.error(accountId, err)
}
created, sessionState, state, lang, jerr := g.jmap.CreateIdentity(accountId, identity, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, IdentityResponseObjectType, state)
})
func (g *Groupware) CreateIdentity(w http.ResponseWriter, r *http.Request) {
create(Identity, w, r, g, nil, g.jmap.CreateIdentity)
}
func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
id, err := req.PathParamDoc(UriParamIdentityId, "The unique identifier of the Identity to modify")
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(UriParamIdentityId, log.SafeString(id)))
ctx := req.ctx.WithLogger(logger)
var identity jmap.IdentityChange
err = req.body(&identity)
if err != nil {
return req.error(accountId, err)
}
updated, sessionState, state, lang, jerr := g.jmap.UpdateIdentity(accountId, id, identity, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, IdentityResponseObjectType, state)
})
modify(Identity, w, r, g, g.jmap.UpdateIdentity)
}
// Delete an identity.
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
id, err := req.PathParam(UriParamIdentityId)
if err != nil {
return req.error(accountId, err)
}
ids := strings.Split(id, ",")
if len(ids) < 1 {
return req.parameterErrorResponse(single(accountId), UriParamIdentityId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
}
logger := log.From(req.logger.With().Str(logAccountId, accountId).Array(UriParamIdentityId, log.SafeStringArray(ids)))
ctx := req.ctx.WithLogger(logger)
notDeleted, sessionState, state, lang, jerr := g.jmap.DeleteIdentity(accountId, ids, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if len(notDeleted) == 0 {
return req.noContent(accountId, sessionState, IdentityResponseObjectType, state)
} else {
logger.Error().Msgf("failed to delete %d identities", len(notDeleted))
return req.errorS(accountId, req.apiError(&ErrorFailedToDeleteSomeIdentities), sessionState)
}
})
delete(Identity, w, r, g, g.jmap.DeleteIdentity)
}
// Get changes to Identities since a given State
// @api:tags identity,changes
func (g *Groupware) GetIdentityChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
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 identity changes"))
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := g.jmap.GetIdentityChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
var body jmap.IdentityChanges = changes
return req.respond(accountId, body, sessionState, IdentityResponseObjectType, state)
})
changes(Identity, w, r, g, g.jmap.GetIdentityChanges)
}

View File

@@ -160,7 +160,7 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
Accounts: buildIndexAccounts(req.session, boot),
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
}
return req.respondN(accountIds, body, sessionState, IndexResponseObjectType, state)
return req.respondN(accountIds, body, sessionState, IndexResponseObjectType, state, lang)
})
}

View File

@@ -18,29 +18,12 @@ import (
//
// This is the primary mechanism for organising Emails within an account.
// It is analogous to a folder or a label in other systems.
func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
func (g *Groupware) GetMailboxById(w http.ResponseWriter, r *http.Request) {
get(Mailbox, w, r, g, g.jmap.GetMailbox)
}
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return req.error(accountId, err)
}
mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, single(mailboxId), req.ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if len(mailboxes.List) == 1 {
return req.respond(accountId, mailboxes.List[0], sessionState, MailboxResponseObjectType, state)
} else {
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
}
})
func (g *Groupware) ModifyMailbox(w http.ResponseWriter, r *http.Request) {
modify(Mailbox, w, r, g, g.jmap.UpdateMailbox)
}
// Get the list of all the mailboxes of an account, potentially filtering on the
@@ -92,7 +75,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state)
return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
}
@@ -102,7 +85,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS
return req.jmapError(accountId, err, sessionState, lang)
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state)
return req.respond(accountId, sortMailboxSlice(mailboxes), sessionState, MailboxResponseObjectType, state, lang)
} else {
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
}
@@ -140,13 +123,13 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
if err != nil {
return req.jmapErrorN(accountIds, err, sessionState, lang)
}
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
} else {
mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes(accountIds, ctx)
if err != nil {
return req.jmapErrorN(accountIds, err, sessionState, lang)
}
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
}
})
}
@@ -175,58 +158,14 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt
if jerr != nil {
return req.jmapErrorN(accountIds, jerr, sessionState, lang)
}
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state)
return req.respondN(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
})
}
// Get the changes tp Mailboxes since a certain State.
// @api:tags mailbox,changes
func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list mailbox changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
// As for Emails and Contacts, one would expect this request without any prior state to
// be usable to list all the objects that currently exist, but that is not the case for
// Mailbox, at least in combination with Stalwart, as those are initial objects that
// "always existed", even with the initial State, and this the Mailbox/changes operation
// returns nothing.
// For this reason, when the "since" state is empty, we respond with an error.
if sinceState == "" {
return req.error(accountId, req.apiError(&ErrorInvalidUserRequest, withTitle(
"Mailbox changes without state is unsupported",
"Requesting Mailbox changes without an initial state is an unsupported operation",
)))
}
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, MailboxResponseObjectType, state)
})
changes(Mailbox, w, r, g, g.jmap.GetMailboxChanges)
}
// Get the changes that occured in all the mailboxes of all accounts.
@@ -266,7 +205,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
return req.jmapErrorN(allAccountIds, jerr, sessionState, lang)
}
return req.respondN(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state)
return req.respondN(allAccountIds, changesByAccountId, sessionState, MailboxResponseObjectType, state, lang)
})
}
@@ -285,67 +224,12 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
return req.jmapErrorN(allAccountIds, jerr, sessionState, lang)
}
return req.respondN(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state)
})
}
func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
mailboxId, err := req.PathParamDoc(UriParamMailboxId, "the identifier of the mailbox to update")
if err != nil {
return req.error(accountId, err)
}
l = l.Str(UriParamMailboxId, log.SafeString(mailboxId))
var body jmap.MailboxChange
err = req.body(&body)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := g.jmap.UpdateMailbox(accountId, mailboxId, body, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, MailboxResponseObjectType, state)
return req.respondN(allAccountIds, rolesByAccountId, sessionState, MailboxResponseObjectType, state, lang)
})
}
func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
var body jmap.MailboxChange
err = req.body(&body)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
created, sessionState, state, lang, jerr := g.jmap.CreateMailbox(accountId, body, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, MailboxResponseObjectType, state)
})
create(Mailbox, w, r, g, nil, g.jmap.CreateMailbox)
}
// Delete Mailboxes by their unique identifiers.
@@ -354,34 +238,7 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
//
// @api:example deletedmailboxes
func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
return req.error(accountId, err)
}
l = l.Str(logAccountId, accountId)
mailboxIds, err := req.PathListParamDoc(UriParamMailboxId, "the identifier of the mailbox to delete")
if err != nil {
return req.error(accountId, err)
}
l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
if len(mailboxIds) < 1 {
return req.noop(accountId) // no mailbox identifiers were mentioned in the request
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, mailboxIds, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, deleted, sessionState, MailboxResponseObjectType, state)
})
delete(Mailbox, w, r, g, g.jmap.DeleteMailboxes)
}
var mailboxRoleSortOrderScore = map[string]int{

View File

@@ -130,6 +130,6 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON
}
var body jmap.Objects = objs
return req.respond(accountId, body, sessionState, "", state)
return req.respond(accountId, body, sessionState, "", state, lang)
})
}

View File

@@ -13,22 +13,8 @@ import (
//
// Note that there may be multiple Quota objects for different resource types.
func (g *Groupware) GetQuota(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)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
ctx := req.ctx.WithLogger(logger)
res, sessionState, state, lang, jerr := g.jmap.GetQuotas(single(accountId), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, quotas := range res {
return req.respond(accountId, quotas, sessionState, QuotaResponseObjectType, state)
}
return req.notFound(accountId, sessionState, QuotaResponseObjectType, state)
getFromMap(Quota, w, r, g, func(accountIds, _ []string, ctx jmap.Context) (map[string]jmap.QuotaGetResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return g.jmap.GetQuotas(accountIds, ctx)
})
}
@@ -62,42 +48,15 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
Quotas: accountQuotas.List,
}
}
return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state)
return req.respondN(accountIds, result, sessionState, QuotaResponseObjectType, state, lang)
})
}
// currently unsupported in Stalwart:
/*
// Get changes to Quotas since a given State
//
// Currently unsupported in Stalwart.
// @api:tags contact,changes
// @api:ignore
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)
})
changes(Quota, w, r, g, g.jmap.GetQuotaChanges)
}
*/

View File

@@ -16,7 +16,7 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
var _ string = accountId
var body []jmap.TaskList = AllTaskLists
return req.respond(accountId, body, req.session.State, TaskListResponseObjectType, TaskListsState)
return req.respond(accountId, body, req.session.State, TaskListResponseObjectType, TaskListsState, jmap.NoLanguage)
})
}
@@ -36,7 +36,7 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
// TODO replace with proper implementation
for _, tasklist := range AllTaskLists {
if tasklist.Id == tasklistId {
return req.respond(accountId, tasklist, req.session.State, TaskListResponseObjectType, TaskListsState)
return req.respond(accountId, tasklist, req.session.State, TaskListResponseObjectType, TaskListsState, jmap.NoLanguage)
}
}
return req.etaggedNotFound(accountId, req.session.State, TaskListResponseObjectType, TaskListsState)
@@ -61,6 +61,6 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
if !ok {
return req.notFound(accountId, req.session.State, TaskResponseObjectType, TaskState)
}
return req.respond(accountId, tasks, req.session.State, TaskResponseObjectType, TaskState)
return req.respond(accountId, tasks, req.session.State, TaskResponseObjectType, TaskState, jmap.NoLanguage)
})
}

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Get vacation notice information.
@@ -14,19 +13,8 @@ import (
//
// The VacationResponse object represents the state of vacation-response-related settings for an account.
func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForVacationResponse()
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
ctx := req.ctx.WithLogger(logger)
res, sessionState, state, lang, jerr := g.jmap.GetVacationResponse(accountId, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state)
get(VacationResponse, w, r, g, func(accountId string, ids []string, ctx jmap.Context) (jmap.VacationResponseGetResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return g.jmap.GetVacationResponse(accountId, ctx)
})
}
@@ -35,25 +23,7 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
// A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original
// sender that their message may not be read for some time.
func (g *Groupware) SetVacation(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForVacationResponse()
if err != nil {
return req.error(accountId, err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
ctx := req.ctx.WithLogger(logger)
var body jmap.VacationResponseChange
err = req.body(&body)
if err != nil {
return req.error(accountId, err)
}
res, sessionState, state, lang, jerr := g.jmap.SetVacationResponse(accountId, body, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, res, sessionState, VacationResponseResponseObjectType, state)
modify(VacationResponse, w, r, g, func(accountId string, id string, change jmap.VacationResponseChange, ctx jmap.Context) (jmap.VacationResponse, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return g.jmap.SetVacationResponse(accountId, change, ctx)
})
}

View File

@@ -191,18 +191,27 @@ const (
ErrorCodeAccountNotFound = "ACCNFD"
ErrorCodeAccountNotSupportedByMethod = "ACCNSM"
ErrorCodeAccountReadOnly = "ACCRDO"
ErrorCodeMissingBlobSessionCapability = "MSCBLO"
ErrorCodeMissingBlobAccountCapability = "MACBLO"
ErrorCodeMissingMailsSessionCapability = "MSCMAI"
ErrorCodeMissingMailsAccountCapability = "MACMAI"
ErrorCodeMissingCalendarsSessionCapability = "MSCCAL"
ErrorCodeMissingCalendarsAccountCapability = "MACCAL"
ErrorCodeMissingContactsSessionCapability = "MSCCON"
ErrorCodeMissingContactsAccountCapability = "MACCON"
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
ErrorCodeMissingTasksAccountCapability = "MACTSK"
ErrorCodeMissingQuotaSessionCapability = "MSCMAI"
ErrorCodeMissingQuotaAccountCapability = "MACMAI"
ErrorCodeFailedToDeleteEmail = "DELEML"
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
ErrorCodeFailedToSanitizeEmail = "FSANEM"
ErrorCodeFailedToDeleteAddressBook = "DELABK"
ErrorCodeFailedToDeleteCalendar = "DELCAL"
ErrorCodeFailedToDeleteMailbox = "DELMBX"
ErrorCodeFailedToDeleteContact = "DELCNT"
ErrorCodeFailedToDeleteCalendar = "DELCAL"
ErrorCodeFailedToDeleteEvent = "DELEVT"
ErrorCodeFailedToDeleteIdentity = "DELIDN"
ErrorCodeNoMailboxWithDraftRole = "NMBXDR"
ErrorCodeNoMailboxWithSentRole = "NMBXSE"
ErrorCodeInvalidSortSpecification = "INVSSP"
@@ -392,6 +401,30 @@ var (
Title: "The referenced Account is read-only",
Detail: "The Account that was referenced in the request only supports read-only operations.",
}
ErrorMissingBlobSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingBlobAccountCapability,
Title: "Session is missing the blob session capability",
Detail: "The JMAP Session of the user does not have the required capability for blobs.",
}
ErrorMissingBlobAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingBlobSessionCapability,
Title: "Account is missing the blob capability",
Detail: "The JMAP Account of the user does not have the required capability for blobs.",
}
ErrorMissingMailsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingMailsAccountCapability,
Title: "Session is missing the mails session capability",
Detail: "The JMAP Session of the user does not have the required capability for mails.",
}
ErrorMissingMailsAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingMailsSessionCapability,
Title: "Account is missing the mails capability",
Detail: "The JMAP Account of the user does not have the required capability for mails.",
}
ErrorMissingCalendarsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingCalendarsAccountCapability,
@@ -428,6 +461,18 @@ var (
Title: "Account is missing the tasks capability",
Detail: "The JMAP Account of the user does not have the required capability for tasks",
}
ErrorMissingQuotaSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingQuotaSessionCapability,
Title: "Session is missing the quota session capability",
Detail: "The JMAP Session of the user does not have the required capability for quotas.",
}
ErrorMissingQuotaAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingQuotaAccountCapability,
Title: "Account is missing the quota capability",
Detail: "The JMAP Account of the user does not have the required capability for quotas.",
}
ErrorFailedToDeleteEmail = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteEmail,
@@ -452,11 +497,17 @@ var (
Title: "Failed to delete address books",
Detail: "One or more address books could not be deleted.",
}
ErrorFailedToDeleteContact = GroupwareError{
ErrorFailedToDeleteMailbox = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteContact,
Title: "Failed to delete contacts",
Detail: "One or more contacts could not be deleted.",
Code: ErrorCodeFailedToDeleteMailbox,
Title: "Failed to delete mailboxes",
Detail: "One or more mailboxes could not be deleted.",
}
ErrorFailedToDeleteEvent = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteEvent,
Title: "Failed to delete events",
Detail: "One or more events could not be deleted.",
}
ErrorFailedToDeleteCalendar = GroupwareError{
Status: http.StatusInternalServerError,
@@ -464,6 +515,18 @@ var (
Title: "Failed to delete calendar",
Detail: "One or more calendars could not be deleted.",
}
ErrorFailedToDeleteContact = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteContact,
Title: "Failed to delete contacts",
Detail: "One or more contacts could not be deleted.",
}
ErrorFailedToDeleteIdentity = GroupwareError{
Status: http.StatusInternalServerError,
Code: ErrorCodeFailedToDeleteIdentity,
Title: "Failed to delete identities",
Detail: "One or more identities could not be deleted.",
}
ErrorNoMailboxWithDraftRole = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeNoMailboxWithDraftRole,

View File

@@ -43,7 +43,6 @@ const (
logErrorSourceHeader = "source-header"
logErrorSourceParameter = "source-parameter"
logErrorSourcePointer = "source-pointer"
logIdentityId = "identity-id"
logEmailId = "email-id"
logJobDescription = "job"
logJobId = "job-id"
@@ -108,7 +107,7 @@ type Groupware struct {
jmap *jmap.Client
userProvider userProvider
// SSE events that need to be pushed to clients.
eventChannel chan Event
eventChannel chan SSEvent
// Background jobs that need to be executed.
jobsChannel chan Job
// A threadsafe counter to generate the job IDs.
@@ -132,8 +131,8 @@ func (e GroupwareInitializationError) Unwrap() error {
return e.Err
}
// SSE Event.
type Event struct {
// Ssrver Sent Event.
type SSEvent struct {
// The type of event, will be sent as the "type" attribute.
Type string
// The ID of the stream to push the event to, typically the username.
@@ -314,7 +313,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
jmapClient.AddSessionEventListener(sessionCache)
// A channel to process SSE Events with a single worker.
eventChannel := make(chan Event, eventChannelSize)
eventChannel := make(chan SSEvent, eventChannelSize)
{
eventBufferSizeMetric, err := prometheus.NewConstMetric(m.EventBufferSizeDesc, prometheus.GaugeValue, float64(eventChannelSize))
if err != nil {
@@ -465,7 +464,7 @@ func (g *Groupware) listenForEvents() {
func (g *Groupware) push(user user, typ string, body any) {
g.metrics.SSEEventsCounter.WithLabelValues(typ).Inc()
g.eventChannel <- Event{Type: typ, Stream: user.GetUsername(), Body: body}
g.eventChannel <- SSEvent{Type: typ, Stream: user.GetUsername(), Body: body}
}
func (g *Groupware) ServeHTTP(w http.ResponseWriter, r *http.Request) {

View File

@@ -0,0 +1,106 @@
package groupware
import (
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
type ObjectType[T jmap.Foo, CH jmap.Change, CHS jmap.Changes[T]] struct {
name string
responseType ResponseObjectType
uriParamName string
containerUriParamName string
accountFunc func(r *Request) (bool, string, Response)
failedToDeleteError GroupwareError
}
var (
Blob = ObjectType[jmap.Blob, jmap.BlobChange, jmap.BlobChanges]{
name: "blob",
responseType: BlobResponseObjectType,
uriParamName: UriParamBlobId,
containerUriParamName: "",
accountFunc: (*Request).needBloblWithAccount,
failedToDeleteError: ErrorServerUnavailable,
}
AddressBook = ObjectType[jmap.AddressBook, jmap.AddressBookChange, jmap.AddressBookChanges]{
name: "address book",
responseType: AddressBookResponseObjectType,
uriParamName: UriParamAddressBookId,
containerUriParamName: "",
accountFunc: (*Request).needContactWithAccount,
failedToDeleteError: ErrorFailedToDeleteAddressBook,
}
Calendar = ObjectType[jmap.Calendar, jmap.CalendarChange, jmap.CalendarChanges]{
name: "calendar",
responseType: CalendarResponseObjectType,
uriParamName: UriParamCalendarId,
containerUriParamName: "",
accountFunc: (*Request).needCalendarWithAccount,
failedToDeleteError: ErrorFailedToDeleteCalendar,
}
Contact = ObjectType[jmap.ContactCard, jmap.ContactCardChange, jmap.ContactCardChanges]{
name: "contact",
responseType: ContactResponseObjectType,
uriParamName: UriParamContactId,
containerUriParamName: UriParamCalendarId,
accountFunc: (*Request).needCalendarWithAccount,
failedToDeleteError: ErrorFailedToDeleteContact,
}
Email = ObjectType[jmap.Email, jmap.EmailChange, jmap.EmailChanges]{
name: "email",
responseType: EmailResponseObjectType,
uriParamName: UriParamEmailId,
containerUriParamName: UriParamMailboxId,
accountFunc: (*Request).needMailWithAccount,
failedToDeleteError: ErrorFailedToDeleteEmail,
}
Event = ObjectType[jmap.CalendarEvent, jmap.CalendarEventChange, jmap.CalendarEventChanges]{
name: "event",
responseType: EventResponseObjectType,
uriParamName: UriParamEventId,
containerUriParamName: UriParamCalendarId,
accountFunc: (*Request).needCalendarWithAccount,
failedToDeleteError: ErrorFailedToDeleteEvent,
}
Identity = ObjectType[jmap.Identity, jmap.IdentityChange, jmap.IdentityChanges]{
name: "identity",
responseType: IdentityResponseObjectType,
uriParamName: UriParamIdentityId,
containerUriParamName: "",
accountFunc: (*Request).needMailWithAccount,
failedToDeleteError: ErrorFailedToDeleteIdentity,
}
Mailbox = ObjectType[jmap.Mailbox, jmap.MailboxChange, jmap.MailboxChanges]{
name: "mailbox",
responseType: MailboxResponseObjectType,
uriParamName: UriParamMailboxId,
containerUriParamName: "",
accountFunc: (*Request).needMailWithAccount,
failedToDeleteError: ErrorFailedToDeleteMailbox,
}
Quota = ObjectType[jmap.Quota, jmap.QuotaChange, jmap.QuotaChanges]{
name: "quota",
responseType: QuotaResponseObjectType,
uriParamName: "",
containerUriParamName: "",
accountFunc: (*Request).needQuotaWithAccount,
failedToDeleteError: ErrorServerUnavailable,
}
VacationResponse = ObjectType[jmap.VacationResponse, jmap.VacationResponseChange, jmap.VacationResponseChanges]{
name: "vacation response",
responseType: VacationResponseResponseObjectType,
uriParamName: "",
containerUriParamName: "",
accountFunc: (*Request).needMailWithAccount,
failedToDeleteError: ErrorServerUnavailable,
}
)

View File

@@ -227,6 +227,16 @@ func (r *Request) parameterErrorResponse(accountIds []string, param string, deta
return r.errorN(accountIds, r.parameterError(param, detail))
}
func (r *Request) unsupportedParams(accountIds []string, params ...string) (bool, Response) {
q := r.r.URL.Query()
for _, p := range params {
if q.Has(p) {
return true, r.parameterErrorResponse(accountIds, p, "Unsupported query parameter")
}
}
return false, Response{}
}
func (r *Request) getStringParam(param string, defaultValue string) (string, bool) {
q := r.r.URL.Query()
if !q.Has(param) {
@@ -373,10 +383,8 @@ func (r *Request) parseOptStringListParam(param string) ([]string, bool, *Error)
return nil, false, nil
}
for _, value := range q[param] {
for _, v := range strings.Split(value, ",") {
if strings.TrimSpace(v) != "" {
result = append(result, v)
}
for v := range notEmptyString(trimmed(strings.SplitSeq(value, ","))) {
result = append(result, v)
}
}
return result, true, nil
@@ -445,6 +453,70 @@ func (r *Request) observeJmapError(jerr jmap.Error) jmap.Error {
return jerr
}
func (r *Request) needBlob(accountId string) (bool, Response) {
if r.session.Capabilities.Blob == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobSessionCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needBlobForAccount(accountId string) (bool, Response) {
if ok, resp := r.needBlob(accountId); !ok {
return ok, resp
}
account, ok := r.session.Accounts[accountId]
if !ok {
return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
}
if account.AccountCapabilities.Blob == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobAccountCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needBloblWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForBlob()
if err != nil {
return false, "", r.error(accountId, err)
}
if ok, resp := r.needBlobForAccount(accountId); !ok {
return false, accountId, resp
}
return true, accountId, Response{}
}
func (r *Request) needMail(accountId string) (bool, Response) {
if r.session.Capabilities.Mail == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsSessionCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needMailForAccount(accountId string) (bool, Response) {
if ok, resp := r.needMail(accountId); !ok {
return ok, resp
}
account, ok := r.session.Accounts[accountId]
if !ok {
return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
}
if account.AccountCapabilities.Contacts == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsAccountCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needMailWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForMail()
if err != nil {
return false, "", r.error(accountId, err)
}
if ok, resp := r.needMailForAccount(accountId); !ok {
return false, accountId, resp
}
return true, accountId, Response{}
}
func (r *Request) needTask(accountId string) (bool, Response) {
if !IgnoreSessionCapabilityChecksForTasks {
if r.session.Capabilities.Tasks == nil {
@@ -543,6 +615,38 @@ func (r *Request) needContactWithAccount() (bool, string, Response) {
return true, accountId, Response{}
}
func (r *Request) needQuota(accountId string) (bool, Response) {
if r.session.Capabilities.Quota == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaSessionCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needQuotaForAccount(accountId string) (bool, Response) {
if ok, resp := r.needQuota(accountId); !ok {
return ok, resp
}
account, ok := r.session.Accounts[accountId]
if !ok {
return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage)
}
if account.AccountCapabilities.Quota == nil {
return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaAccountCapability), r.session.State, jmap.NoLanguage)
}
return true, Response{}
}
func (r *Request) needQuotaWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForQuota()
if err != nil {
return false, "", r.error(accountId, err)
}
if ok, resp := r.needQuotaForAccount(accountId); !ok {
return false, accountId, resp
}
return true, accountId, Response{}
}
type SortCrit struct {
Attribute string
Ascending bool
@@ -598,7 +702,3 @@ func mapSort[T any](accountIds []string, req *Request, defaultSort []T, props []
func toState(s string) jmap.State {
return jmap.State(s)
}
func ptr[T any](t T) *T {
return &t
}

View File

@@ -74,12 +74,12 @@ func etaggedResponse(accountIds []string, body any, sessionState jmap.SessionSta
}
}
func (r *Request) respond(accountId string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response {
return etaggedResponse(single(accountId), body, sessionState, objectType, etag, jmap.Language(r.language()))
func (r *Request) respond(accountId string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, lang jmap.Language) Response {
return etaggedResponse(single(accountId), body, sessionState, objectType, etag, lang)
}
func (r *Request) respondN(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response {
return etaggedResponse(accountIds, body, sessionState, objectType, etag, jmap.Language(r.language()))
func (r *Request) respondN(accountIds []string, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, lang jmap.Language) Response {
return etaggedResponse(accountIds, body, sessionState, objectType, etag, lang)
}
/*

View File

@@ -67,6 +67,8 @@ const (
QueryParamQuotas = "quotas"
QueryParamIdentities = "identities"
QueryParamEmailSubmissions = "submissions"
QueryParamId = "id"
QueryParamCalculateTotal = "total"
HeaderParamSince = "if-none-match"
)
@@ -91,27 +93,31 @@ func (g *Groupware) Route(r chi.Router) {
})
})
r.Route("/{accountid}", func(r chi.Router) {
r.Get("/", g.GetAccount)
r.Get("/", g.GetAccountById)
r.Route("/identities", func(r chi.Router) {
r.Get("/", g.GetIdentities)
r.Post("/", g.AddIdentity)
r.Post("/", g.CreateIdentity)
r.Route("/{identityid}", func(r chi.Router) {
r.Get("/", g.GetIdentityById)
r.Patch("/", g.ModifyIdentity)
r.Delete("/", g.DeleteIdentity)
})
})
r.Get("/vacation", g.GetVacation)
r.Put("/vacation", g.SetVacation)
r.Get("/quota", g.GetQuota)
r.Route("/vacation", func(r chi.Router) {
r.Get("/", g.GetVacation)
r.Put("/", g.SetVacation)
})
r.Route("/quota", func(r chi.Router) {
r.Get("/", g.GetQuota)
})
r.Route("/mailboxes", func(r chi.Router) {
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
r.Post("/", g.CreateMailbox)
r.Route("/{mailboxid}", func(r chi.Router) {
r.Get("/", g.GetMailbox)
r.Get("/emails", g.GetAllEmailsInMailbox)
r.Patch("/", g.UpdateMailbox)
r.Get("/", g.GetMailboxById)
r.Patch("/", g.ModifyMailbox)
r.Delete("/", g.DeleteMailbox)
r.Get("/emails", g.GetAllEmailsInMailbox)
})
})
r.Route("/emails", func(r chi.Router) {
@@ -150,7 +156,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetAddressbooks)
r.Post("/", g.CreateAddressBook)
r.Route("/{addressbookid}", func(r chi.Router) {
r.Get("/", g.GetAddressbook)
r.Get("/", g.GetAddressbookById)
r.Patch("/", g.ModifyAddressBook)
r.Delete("/", g.DeleteAddressBook)
r.Get("/contacts", g.GetContactsInAddressbook) //NOSONAR
@@ -176,9 +182,13 @@ func (g *Groupware) Route(r chi.Router) {
})
})
r.Route("/events", func(r chi.Router) {
r.Post("/", g.CreateCalendarEvent)
r.Patch("/", g.ModifyCalendarEvent)
r.Delete("/{eventid}", g.DeleteCalendarEvent)
r.Get("/", g.GetAllEvents)
r.Post("/", g.CreateEvent)
r.Route("/{eventid}", func(r chi.Router) {
r.Get("/", g.GetEventById)
r.Patch("/", g.ModifyEvent)
r.Delete("/", g.DeleteEvent)
})
})
r.Route("/tasklists", func(r chi.Router) {
r.Get("/", g.GetTaskLists)

View File

@@ -0,0 +1,469 @@
package groupware
import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
// Create a new {{.Name}} using the JSON payload in the body if the `{{.Method}}` operation.
//
// When successful, it returns a `200 OK` with the {{.ObjType}} that was just created in the response.
func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
bodyFunc func(r Request, accountId string, body *CHANGE, ctx jmap.Context) (bool, Response),
createFunc func(accountId string, change CHANGE, ctx jmap.Context) (*T, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
var create CHANGE
err := req.body(&create)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
if bodyFunc != nil {
if ok, resp := bodyFunc(req, accountId, &create, ctx); !ok {
return resp
}
}
created, sessionState, state, lang, jerr := createFunc(accountId, create, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, created, sessionState, o.responseType, state, lang)
})
}
func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
if notok, resp := req.unsupportedParams(single(accountId), QueryParamOffset, QueryParamLimit); notok {
return resp
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
objs, sessionState, state, lang, jerr := getFunc(accountId, []string{}, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, objs, sessionState, o.responseType, state, lang)
})
}
func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T], FILTER any, COMP any, SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
filterFunc func(containerId string) FILTER,
sortBy []COMP,
queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, offset int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
search := false
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
search = true
l = l.Int(QueryParamOffset, offset)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, uint(0))
if err != nil {
return req.error(accountId, err)
}
if ok {
search = true
l = l.Uint(QueryParamLimit, limit)
}
if search {
containerId := ""
if o.containerUriParamName != "" {
var err *Error
containerId, err = req.PathParam(o.containerUriParamName)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(o.containerUriParamName, log.SafeString(containerId))
}
filter := filterFunc(containerId)
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, offset, limit, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, results, sessionState, o.responseType, state, lang)
} else {
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
objs, sessionState, state, lang, jerr := getFunc(accountId, []string{}, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, objs, sessionState, o.responseType, state, lang)
}
})
}
func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
defaultLimit uint,
queryFunc func(req Request, accountId string, containerId string, offset int, limit uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, *Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
containerId := ""
if o.containerUriParamName != "" {
var err *Error
containerId, err = req.PathParam(o.containerUriParamName)
if err != nil {
return req.error(accountId, err)
}
l = l.Str(o.containerUriParamName, log.SafeString(containerId))
}
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Int(QueryParamOffset, offset)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, defaultLimit)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamLimit, limit)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, offset, limit, ctx)
if err != nil {
return req.error(accountId, err)
}
return req.respond(accountId, results, sessionState, o.responseType, state, lang)
})
}
func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
getFunc func(accountId string, ids []string, ctx jmap.Context) (RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
ids := []string{}
if o.uriParamName != "" {
id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve")
if err != nil {
return req.error(accountId, err)
}
l.Str(o.uriParamName, log.SafeString(id))
ids = single(id)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
objs, sessionState, state, lang, jerr := getFunc(accountId, ids, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
n := len(objs.GetList())
switch n {
case 0:
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
case 1:
return req.respond(accountId, objs.GetList()[0], sessionState, ContactResponseObjectType, state, lang)
default:
logger.Error().Msgf("found %d %s matching '%s' instead of 1", n, o.responseType, ids)
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
}
})
}
func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
getFunc func(accountIds []string, ids []string, ctx jmap.Context) (map[string]RESP, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve")
// TODO add id splitting
if err != nil {
return req.error(accountId, err)
}
l.Str(o.uriParamName, log.SafeString(id))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
objMap, sessionState, state, lang, jerr := getFunc(single(accountId), single(id), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
if objs, ok := objMap[accountId]; ok {
n := len(objs.GetList())
switch n {
case 0:
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
case 1:
return req.respond(accountId, objs.GetList()[0], sessionState, ContactResponseObjectType, state, lang)
default:
logger.Error().Msgf("found %d %s matching '%s' instead of 1", n, o.responseType, id)
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
}
} else {
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
}
})
}
func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
changesFunc func(accountId string, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (CHANGES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState := jmap.State(req.OptHeaderParamDoc(HeaderParamSince, "Optionally specifies the state identifier from which on to list changes"))
if sinceState != "" {
l = l.Str(HeaderParamSince, log.SafeString(string(sinceState)))
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
changes, sessionState, state, lang, jerr := changesFunc(accountId, sinceState, maxChanges, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, changes, sessionState, o.responseType, state, lang)
})
}
func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
deleteFunc func(accountId string, ids []string, ctx jmap.Context) (map[string]jmap.SetError, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to delete")
if err != nil {
return req.error(accountId, err)
}
l.Str(o.uriParamName, log.SafeString(id))
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, single(id), ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range setErrors {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
o.failedToDeleteError,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
o.failedToDeleteError,
))
}
}
return req.noContent(accountId, sessionState, o.responseType, state)
})
}
func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
deleteFunc func(accountId string, ids []string, ctx jmap.Context) (map[string]jmap.SetError, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
ids := []string{}
if o.uriParamName != "" {
pathId, err := req.PathParam(o.uriParamName)
if err != nil {
return req.error(accountId, err)
}
if ok {
ids = append(ids, pathId)
}
}
{
queryIds, ok, err := req.parseOptStringListParam(QueryParamId)
if err != nil {
return req.error(accountId, err)
}
if ok {
ids = append(ids, queryIds...)
}
}
{
var bodyIds []string
err := req.body(&bodyIds)
if err != nil {
return req.error(accountId, err)
}
ids = append(ids, bodyIds...)
}
switch len(ids) {
case 0:
return req.noop(accountId)
case 1:
l.Str("id", log.SafeString(ids[0]))
default:
l.Array("ids", log.SafeStringArray(ids))
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
setErrors, sessionState, state, lang, jerr := deleteFunc(accountId, ids, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
for _, e := range setErrors {
desc := e.Description
if desc != "" {
return req.error(accountId, apiError(
req.errorId(),
o.failedToDeleteError,
withDetail(e.Description),
))
} else {
return req.error(accountId, apiError(
req.errorId(),
o.failedToDeleteError,
))
}
}
return req.noContent(accountId, sessionState, o.responseType, state)
})
}
func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
updateFunc func(accountId string, id string, change CHANGE, ctx jmap.Context) (T, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
if !ok {
return resp
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to modify")
if err != nil {
return req.error(accountId, err)
}
l.Str(o.uriParamName, log.SafeString(id))
var change CHANGE
err = req.body(&change)
if err != nil {
return req.error(accountId, err)
}
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
updated, sessionState, state, lang, jerr := updateFunc(accountId, id, change, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
return req.respond(accountId, updated, sessionState, o.responseType, state, lang)
})
}

View File

@@ -0,0 +1,20 @@
package groupware
import (
"iter"
"strings"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
func ptr[T any](t T) *T {
return &t
}
func trimmed(it iter.Seq[string]) iter.Seq[string] {
return structs.MapSeq(it, strings.TrimSpace)
}
func notEmptyString(it iter.Seq[string]) iter.Seq[string] {
return structs.FilterSeq(it, func(s string) bool { return s != "" })
}