From 0cebbbd83f2794424a11d363fcb66fb632cc94ba Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 2 Oct 2025 17:02:52 +0200
Subject: [PATCH] groupware: add JMAP capability checking (in part: for
contacts, calendars, tasks)
---
pkg/jmap/jmap_model.go | 234 ++++++++++++++----
pkg/jmap/jmap_test.go | 46 +++-
.../pkg/groupware/groupware_api_calendars.go | 18 ++
.../pkg/groupware/groupware_api_contacts.go | 18 ++
.../pkg/groupware/groupware_api_tasklists.go | 18 ++
.../pkg/groupware/groupware_error.go | 50 ++++
.../pkg/groupware/groupware_request.go | 111 +++++++++
.../pkg/groupware/groupware_response.go | 9 +
8 files changed, 459 insertions(+), 45 deletions(-)
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 6335671544..e068f124a0 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -131,20 +131,28 @@ type ActionMode string
type SendingMode string
type DispositionTypeOption string
+type Duration string
+
const (
- JmapCore = "urn:ietf:params:jmap:core"
- JmapMail = "urn:ietf:params:jmap:mail"
- JmapMDN = "urn:ietf:params:jmap:mdn" // https://datatracker.ietf.org/doc/rfc9007/
- JmapSubmission = "urn:ietf:params:jmap:submission"
- JmapVacationResponse = "urn:ietf:params:jmap:vacationresponse"
- JmapCalendars = "urn:ietf:params:jmap:calendars"
- JmapContacts = "urn:ietf:params:jmap:contacts"
- JmapSieve = "urn:ietf:params:jmap:sieve"
- JmapBlob = "urn:ietf:params:jmap:blob"
- JmapQuota = "urn:ietf:params:jmap:quota"
- JmapWebsocket = "urn:ietf:params:jmap:websocket"
- JmapPrincipals = "urn:ietf:params:jmap:principals"
- JmapPrincipalsOwner = "urn:ietf:params:jmap:principals:owner"
+ JmapCore = "urn:ietf:params:jmap:core"
+ JmapMail = "urn:ietf:params:jmap:mail"
+ JmapMDN = "urn:ietf:params:jmap:mdn" // https://datatracker.ietf.org/doc/rfc9007/
+ JmapSubmission = "urn:ietf:params:jmap:submission"
+ JmapVacationResponse = "urn:ietf:params:jmap:vacationresponse"
+ JmapCalendars = "urn:ietf:params:jmap:calendars"
+ JmapContacts = "urn:ietf:params:jmap:contacts"
+ JmapSieve = "urn:ietf:params:jmap:sieve"
+ JmapBlob = "urn:ietf:params:jmap:blob"
+ JmapQuota = "urn:ietf:params:jmap:quota"
+ JmapWebsocket = "urn:ietf:params:jmap:websocket"
+ JmapPrincipals = "urn:ietf:params:jmap:principals"
+ JmapPrincipalsOwner = "urn:ietf:params:jmap:principals:owner"
+ JmapTasks = "urn:ietf:params:jmap:tasks"
+ JmapTasksRecurrences = "urn:ietf:params:jmap:tasks:recurrences"
+ JmapTasksAssignees = "urn:ietf:params:jmap:tasks:assignees"
+ JmapTasksAlerts = "urn:ietf:params:jmap:tasks:alerts"
+ JmapTasksMultilingual = "urn:ietf:params:jmap:tasks:multilingual"
+ JmapTasksCustomTimezones = "urn:ietf:params:jmap:tasks:customtimezones"
CoreType = ObjectType("Core")
PushSubscriptionType = ObjectType("PushSubscription")
@@ -584,6 +592,42 @@ type SessionContactsAccountCapabilities struct {
MayCreateAddressBook bool `json:"mayCreateAddressBook"`
}
+type SessionCalendarsAccountCapabilities struct {
+}
+
+type SessionCalendarsParseAccountCapabilities struct {
+}
+
+type SessionTasksAccountCapabilities struct {
+ // The earliest date-time the server is willing to accept for any date stored in a Task.
+ MinDateTime LocalDate `json:"minDateTime,omitzero"`
+
+ // The latest date-time the server is willing to accept for any date stored in a Task.
+ MaxDateTime LocalDate `json:"maxDateTime,omitzero"`
+
+ // If true, the user may create a task list in this account.
+ MayCreateTaskList bool `json:"mayCreateTaskList"`
+}
+
+type SessionTasksRecurrencesAccountCapabilities struct {
+ // The maximum duration the user may query over when asking the server to expand recurrences.
+ MaxExpandedQueryDuration Duration `json:"maxExpandedQueryDuration"`
+}
+
+type SessionTasksAssigneesAccountCapabilities struct {
+ // The maximum number of participants a single task may have, or null for no limit.
+ MaxParticipantsPerTask *uint `json:"maxParticipantsPerTask,omitzero"`
+}
+
+type SessionTasksAlertsAccountCapabilities struct {
+}
+
+type SessionTasksMultilingualAccountCapabilities struct {
+}
+
+type SessionTasksCustomTimezonesAccountCapabilities struct {
+}
+
type SessionPrincipalsAccountCapabilities struct {
// The id of the principal in this account that corresponds to the user fetching this object, if any.
CurrentUserPrincipalId string `json:"currentUserPrincipalId,omitempty"`
@@ -598,20 +642,34 @@ type SessionPrincipalsOwnerAccountCapabilities struct {
PrincipalId string `json:"principalId,omitempty"`
}
+type SessionPrincipalAvailabilityAccountCapabilities struct {
+ // The maximum duration over which the server is prepared to calculate availability in a single call.
+ MaxAvailabilityDuration Duration `json:"maxAvailabilityDuration"`
+}
+
type SessionMDNAccountCapabilities struct {
}
type SessionAccountCapabilities struct {
- Mail *SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail,omitempty"`
- Submission *SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission,omitempty"`
- VacationResponse *SessionVacationResponseAccountCapabilities `json:"urn:ietf:params:jmap:vacationresponse,omitempty"`
- Sieve *SessionSieveAccountCapabilities `json:"urn:ietf:params:jmap:sieve,omitempty"`
- Blob *SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob,omitempty"`
- Quota *SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota,omitempty"`
- Contacts *SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts,omitempty"`
- Principals *SessionPrincipalsAccountCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
- PrincipalsOwner *SessionPrincipalsOwnerAccountCapabilities `json:"urn:ietf:params:jmap:principals:owner,omitempty"`
- MDN *SessionMDNAccountCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
+ Mail *SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail,omitempty"`
+ Submission *SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission,omitempty"`
+ VacationResponse *SessionVacationResponseAccountCapabilities `json:"urn:ietf:params:jmap:vacationresponse,omitempty"`
+ Sieve *SessionSieveAccountCapabilities `json:"urn:ietf:params:jmap:sieve,omitempty"`
+ Blob *SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob,omitempty"`
+ Quota *SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota,omitempty"`
+ Contacts *SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts,omitempty"`
+ Calendars *SessionCalendarsAccountCapabilities `json:"urn:ietf:params:jmap:calendars,omitempty"`
+ CalendarsParse *SessionCalendarsParseAccountCapabilities `json:"urn:ietf:params:jmap:calendars:parse,omitempty"`
+ Tasks *SessionTasksAccountCapabilities `json:"urn:ietf:params:jmap:tasks,omitempty"`
+ TasksRecurrences *SessionTasksRecurrencesAccountCapabilities `json:"urn:ietf:params:jmap:tasks:recurrences,omitempty"`
+ TasksAssignees *SessionTasksAssigneesAccountCapabilities `json:"urn:ietf:params:jmap:tasks:assignees,omitempty"`
+ TasksAlerts *SessionTasksAlertsAccountCapabilities `json:"urn:ietf:params:jmap:tasks:alerts,omitempty"`
+ TasksMultilingual *SessionTasksMultilingualAccountCapabilities `json:"urn:ietf:params:jmap:tasks:multilingual,omitempty"`
+ TasksCustomTimezones *SessionTasksCustomTimezonesAccountCapabilities `json:"urn:ietf:params:jmap:tasks:customtimezones,omitempty"`
+ Principals *SessionPrincipalsAccountCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
+ PrincipalsOwner *SessionPrincipalsOwnerAccountCapabilities `json:"urn:ietf:params:jmap:principals:owner,omitempty"`
+ PrincipalsAvailability *SessionPrincipalAvailabilityAccountCapabilities `json:"urn:ietf:params:jmap:principals:availability,omitempty"`
+ MDN *SessionMDNAccountCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
}
type Account struct {
@@ -644,7 +702,7 @@ type SessionCoreCapabilities struct {
MaxObjectsInSet int `json:"maxObjectsInSet"`
// A list of identifiers for algorithms registered in the collation registry, as defined in [@!RFC4790], that the server
// supports for sorting when querying records.
- CollationAlgorithms []string `json:"collationAlgorithms"`
+ CollationAlgorithms []string `json:"collationAlgorithms,omitempty"`
}
type SessionMailCapabilities struct {
@@ -682,35 +740,125 @@ type SessionWebsocketCapabilities struct {
type SessionContactsCapabilities struct {
}
+type SessionCalendarsCapabilities struct {
+}
+
+type SessionCalendarsParseCapabilities struct {
+}
+
+type SessionTasksCapabilities struct {
+}
+
+type SessionTasksRecurrencesCapabilities struct {
+}
+
+type SessionTasksAssigneesCapabilities struct {
+}
+
+type SessionTasksAlertsCapabilities struct {
+}
+
+type SessionTasksMultilingualCapabilities struct {
+}
+
+type SessionTasksCustomTimezonesCapabilities struct {
+}
+
type SessionPrincipalCapabilities struct {
}
+type SessionPrincipalAvailabilityCapabilities struct {
+}
+
type SessionMDNCapabilities struct {
}
type SessionCapabilities struct {
- Core SessionCoreCapabilities `json:"urn:ietf:params:jmap:core"`
- Mail SessionMailCapabilities `json:"urn:ietf:params:jmap:mail"`
- Submission SessionSubmissionCapabilities `json:"urn:ietf:params:jmap:submission"`
- VacationResponse SessionVacationResponseCapabilities `json:"urn:ietf:params:jmap:vacationresponse"`
- Sieve SessionSieveCapabilities `json:"urn:ietf:params:jmap:sieve"`
- Blob SessionBlobCapabilities `json:"urn:ietf:params:jmap:blob"`
- Quota SessionQuotaCapabilities `json:"urn:ietf:params:jmap:quota"`
- Websocket SessionWebsocketCapabilities `json:"urn:ietf:params:jmap:websocket"`
- Contacts *SessionContactsCapabilities `json:"urn:ietf:params:jmap:contacts"`
- Principals *SessionPrincipalCapabilities `json:"urn:ietf:params:jmap:principals"`
- MDN *SessionMDNCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
+ Core *SessionCoreCapabilities `json:"urn:ietf:params:jmap:core,omitempty"`
+ Mail *SessionMailCapabilities `json:"urn:ietf:params:jmap:mail,omitempty"`
+ Submission *SessionSubmissionCapabilities `json:"urn:ietf:params:jmap:submission,omitempty"`
+ VacationResponse *SessionVacationResponseCapabilities `json:"urn:ietf:params:jmap:vacationresponse,omitempty"`
+ Sieve *SessionSieveCapabilities `json:"urn:ietf:params:jmap:sieve,omitempty"`
+ Blob *SessionBlobCapabilities `json:"urn:ietf:params:jmap:blob,omitempty"`
+ Quota *SessionQuotaCapabilities `json:"urn:ietf:params:jmap:quota,omitempty"`
+ Websocket *SessionWebsocketCapabilities `json:"urn:ietf:params:jmap:websocket,omitempty"`
+
+ // This represents support for the `AddressBook` and `ContactCard` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ Contacts *SessionContactsCapabilities `json:"urn:ietf:params:jmap:contacts,omitempty"`
+
+ // This represents support for the `Calendar`, `CalendarEvent`, `CalendarEventNotification`,
+ // and `ParticipantIdentity` data types and associated API methods, except for `CalendarEvent/parse`.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ Calendars *SessionCalendarsCapabilities `json:"urn:ietf:params:jmap:calendars,omitempty"`
+
+ // This represents support for the `CalendarEvent/parse` method.
+ //
+ // The value of this property is an empty object in the JMAP session `capabilities` property.
+ CalendarsParse *SessionCalendarsParseCapabilities `json:"urn:ietf:params:jmap:calendars:parse,omitempty"`
+
+ // This represents support for the core properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ Tasks *SessionTasksCapabilities `json:"urn:ietf:params:jmap:tasks,omitempty"`
+
+ // This represents support for the `recurrence` properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ TasksRecurrences *SessionTasksRecurrencesCapabilities `json:"urn:ietf:params:jmap:tasks:recurrences,omitempty"`
+
+ // This represents support for the `assignee` properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ TasksAssignees *SessionTasksAssigneesCapabilities `json:"urn:ietf:params:jmap:tasks:assignees,omitempty"`
+
+ // This represents support for the `alerts` properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ TasksAlerts *SessionTasksAlertsCapabilities `json:"urn:ietf:params:jmap:tasks:alerts,omitempty"`
+
+ // This represents support for the multilingual properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ TasksMultilingual *SessionTasksMultilingualCapabilities `json:"urn:ietf:params:jmap:tasks:multilingual,omitempty"`
+
+ // This represents support for the custom time zone properties and objects of the `TaskList`,
+ // `Task` and `TaskNotification` data types and associated API methods.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ TasksCustomTimezones *SessionTasksCustomTimezonesCapabilities `json:"urn:ietf:params:jmap:tasks:customtimezones,omitempty"`
+
+ Principals *SessionPrincipalCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
+
+ // Represents support for the `Principal/getAvailability` method.
+ //
+ // Any account with this capability MUST also have the `urn:ietf:params:jmap:principals` capability.
+ //
+ // The value of this property in the JMAP Session `capabilities` property is an empty object.
+ PrincipalsAvailability *SessionPrincipalAvailabilityCapabilities `json:"urn:ietf:params:jmap:principals:availability,omitempty"`
+
+ MDN *SessionMDNCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"`
}
type SessionPrimaryAccounts struct {
- Core string `json:"urn:ietf:params:jmap:core"`
- Mail string `json:"urn:ietf:params:jmap:mail"`
- Submission string `json:"urn:ietf:params:jmap:submission"`
- VacationResponse string `json:"urn:ietf:params:jmap:vacationresponse"`
- Sieve string `json:"urn:ietf:params:jmap:sieve"`
- Blob string `json:"urn:ietf:params:jmap:blob"`
- Quota string `json:"urn:ietf:params:jmap:quota"`
- Websocket string `json:"urn:ietf:params:jmap:websocket"`
+ Core string `json:"urn:ietf:params:jmap:core,omitempty"`
+ Mail string `json:"urn:ietf:params:jmap:mail,omitempty"`
+ Submission string `json:"urn:ietf:params:jmap:submission,omitempty"`
+ VacationResponse string `json:"urn:ietf:params:jmap:vacationresponse,omitempty"`
+ Sieve string `json:"urn:ietf:params:jmap:sieve,omitempty"`
+ Blob string `json:"urn:ietf:params:jmap:blob,omitempty"`
+ Quota string `json:"urn:ietf:params:jmap:quota,omitempty"`
+ Websocket string `json:"urn:ietf:params:jmap:websocket,omitempty"`
+ Task string `json:"urn:ietf:params:jmap:task,omitempty"`
+ Calendar string `json:"urn:ietf:params:jmap:calendar,omitempty"`
+ Contact string `json:"urn:ietf:params:jmap:contact,omitempty"`
}
type SessionState string
diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go
index 88ac0f7857..cd3fcdd2ea 100644
--- a/pkg/jmap/jmap_test.go
+++ b/pkg/jmap/jmap_test.go
@@ -20,6 +20,48 @@ import (
"github.com/stretchr/testify/require"
)
+func jsoneq[X any](t *testing.T, expected string, object X) {
+ data, err := json.MarshalIndent(object, "", "")
+ require.NoError(t, err)
+ require.JSONEq(t, expected, string(data))
+
+ var rec X
+ err = json.Unmarshal(data, &rec)
+ require.NoError(t, err)
+ require.Equal(t, object, rec)
+}
+
+func TestEmptySessionCapabilitiesMarshalling(t *testing.T) {
+ jsoneq(t, `{}`, SessionCapabilities{})
+}
+
+func TestSessionCapabilitiesMarshalling(t *testing.T) {
+ jsoneq(t, `{
+ "urn:ietf:params:jmap:core": {
+ "maxSizeUpload": 123,
+ "maxConcurrentUpload": 4,
+ "maxSizeRequest": 1000,
+ "maxConcurrentRequests": 8,
+ "maxCallsInRequest": 16,
+ "maxObjectsInGet": 32,
+ "maxObjectsInSet": 8
+ },
+ "urn:ietf:params:jmap:tasks": {
+ }
+ }`, SessionCapabilities{
+ Core: &SessionCoreCapabilities{
+ MaxSizeUpload: 123,
+ MaxConcurrentUpload: 4,
+ MaxSizeRequest: 1000,
+ MaxConcurrentRequests: 8,
+ MaxCallsInRequest: 16,
+ MaxObjectsInGet: 32,
+ MaxObjectsInSet: 8,
+ },
+ Tasks: &SessionTasksCapabilities{},
+ })
+}
+
type TestJmapWellKnownClient struct {
t *testing.T
}
@@ -48,7 +90,7 @@ func (t *TestJmapWellKnownClient) GetSession(sessionUrl *url.URL, username strin
Websocket: pa,
},
Capabilities: SessionCapabilities{
- Core: SessionCoreCapabilities{
+ Core: &SessionCoreCapabilities{
MaxCallsInRequest: 64,
},
},
@@ -182,7 +224,7 @@ func TestRequests(t *testing.T) {
JmapUrl: *jmapUrl,
SessionResponse: SessionResponse{
Capabilities: SessionCapabilities{
- Core: SessionCoreCapabilities{
+ Core: &SessionCoreCapabilities{
MaxCallsInRequest: 10,
},
},
diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go
index c9bfa351f3..732b78cd5a 100644
--- a/services/groupware/pkg/groupware/groupware_api_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_api_calendars.go
@@ -25,6 +25,12 @@ type SwaggerGetCalendars200 struct {
// 500: ErrorResponse500
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
+ }
+ var _ string = accountId
+
return response(AllCalendars, req.session.State)
})
}
@@ -49,6 +55,12 @@ type SwaggerGetCalendarById200 struct {
// 500: ErrorResponse500
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
+ }
+ var _ string = accountId
+
calendarId := chi.URLParam(r, UriParamCalendarId)
// TODO replace with proper implementation
for _, calendar := range AllCalendars {
@@ -78,6 +90,12 @@ type SwaggerGetEventsInCalendar200 struct {
// 500: ErrorResponse500
func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needCalendarWithAccount()
+ if !ok {
+ return resp
+ }
+ var _ string = accountId
+
calendarId := chi.URLParam(r, UriParamCalendarId)
// TODO replace with proper implementation
events, ok := EventsMapByCalendarId[calendarId]
diff --git a/services/groupware/pkg/groupware/groupware_api_contacts.go b/services/groupware/pkg/groupware/groupware_api_contacts.go
index 7eca524aad..75579d3e74 100644
--- a/services/groupware/pkg/groupware/groupware_api_contacts.go
+++ b/services/groupware/pkg/groupware/groupware_api_contacts.go
@@ -26,6 +26,12 @@ type SwaggerGetAddressbooks200 struct {
// 500: ErrorResponse500
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
+ }
+ var _ string = accountId
+
// TODO replace with proper implementation
return response(AllAddressBooks, req.session.State)
})
@@ -51,6 +57,12 @@ type SwaggerGetAddressbookById200 struct {
// 500: ErrorResponse500
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
+ }
+ var _ string = accountId
+
addressBookId := chi.URLParam(r, UriParamAddressBookId)
// TODO replace with proper implementation
for _, ab := range AllAddressBooks {
@@ -80,6 +92,12 @@ type SwaggerGetContactsInAddressbook200 struct {
// 500: ErrorResponse500
func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needContactWithAccount()
+ if !ok {
+ return resp
+ }
+ var _ string = accountId
+
addressBookId := chi.URLParam(r, UriParamAddressBookId)
// TODO replace with proper implementation
contactCards, ok := ContactsMapByAddressBookId[addressBookId]
diff --git a/services/groupware/pkg/groupware/groupware_api_tasklists.go b/services/groupware/pkg/groupware/groupware_api_tasklists.go
index 878935cc1f..e1b9511f77 100644
--- a/services/groupware/pkg/groupware/groupware_api_tasklists.go
+++ b/services/groupware/pkg/groupware/groupware_api_tasklists.go
@@ -25,6 +25,12 @@ type SwaggerGetTaskLists200 struct {
// 500: ErrorResponse500
func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needTaskWithAccount()
+ if !ok {
+ return resp
+ }
+ var _ string = accountId
+
return response(AllTaskLists, req.session.State)
})
}
@@ -49,6 +55,12 @@ type SwaggerGetTaskListById200 struct {
// 500: ErrorResponse500
func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needTaskWithAccount()
+ if !ok {
+ return resp
+ }
+ var _ string = accountId
+
tasklistId := chi.URLParam(r, UriParamTaskListId)
// TODO replace with proper implementation
for _, tasklist := range AllTaskLists {
@@ -78,6 +90,12 @@ type SwaggerGetTasksInTaskList200 struct {
// 500: ErrorResponse500
func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needTaskWithAccount()
+ if !ok {
+ return resp
+ }
+ var _ string = accountId
+
tasklistId := chi.URLParam(r, UriParamTaskListId)
// TODO replace with proper implementation
tasks, ok := TaskMapByTaskListId[tasklistId]
diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go
index 0039cb7fd7..a80c7b4be5 100644
--- a/services/groupware/pkg/groupware/groupware_error.go
+++ b/services/groupware/pkg/groupware/groupware_error.go
@@ -188,6 +188,12 @@ const (
ErrorCodeAccountNotFound = "ACCNFD"
ErrorCodeAccountNotSupportedByMethod = "ACCNSM"
ErrorCodeAccountReadOnly = "ACCRDO"
+ ErrorCodeMissingCalendarsSessionCapability = "MSCCAL"
+ ErrorCodeMissingCalendarsAccountCapability = "MACCAL"
+ ErrorCodeMissingContactsSessionCapability = "MSCCON"
+ ErrorCodeMissingContactsAccountCapability = "MACCON"
+ ErrorCodeMissingTasksSessionCapability = "MSCTSK"
+ ErrorCodeMissingTaskAccountCapability = "MACTSK"
)
var (
@@ -371,6 +377,42 @@ var (
Title: "The referenced Account is read-only",
Detail: "The Account that was referenced in the request only supports read-only operations.",
}
+ ErrorMissingCalendarsSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingCalendarsSessionCapability,
+ Title: "Session is missing the task capability '" + jmap.JmapCalendars + "'",
+ Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ }
+ ErrorMissingCalendarsAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingCalendarsSessionCapability,
+ Title: "Account is missing the task capability '" + jmap.JmapCalendars + "'",
+ Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ }
+ ErrorMissingContactsSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingContactsSessionCapability,
+ Title: "Session is missing the task capability '" + jmap.JmapContacts + "'",
+ Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapContacts + "'.",
+ }
+ ErrorMissingContactsAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingContactsSessionCapability,
+ Title: "Account is missing the task capability '" + jmap.JmapContacts + "'",
+ Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapContacts + "'.",
+ }
+ ErrorMissingTasksSessionCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingTasksSessionCapability,
+ Title: "Session is missing the task capability '" + jmap.JmapTasks + "'",
+ Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ }
+ ErrorMissingTasksAccountCapability = GroupwareError{
+ Status: http.StatusExpectationFailed,
+ Code: ErrorCodeMissingTasksSessionCapability,
+ Title: "Account is missing the task capability '" + jmap.JmapTasks + "'",
+ Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
+ }
)
type ErrorOpt interface {
@@ -527,6 +569,14 @@ func (r Request) observedParameterError(gwerr GroupwareError, options ...ErrorOp
return r.observeParameterError(apiError(r.errorId(), gwerr, options...))
}
+func (r Request) apiError(err *GroupwareError) *Error {
+ if err == nil {
+ return nil
+ }
+ errorId := r.errorId()
+ return apiError(errorId, *err)
+}
+
func (r Request) apiErrorFromJmap(err jmap.Error) *Error {
if err == nil {
return nil
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index ca83c987d5..8ba2ef9c56 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -57,6 +57,9 @@ var (
errNoPrimaryAccountForBlob = errors.New("no primary account for blob")
errNoPrimaryAccountForVacationResponse = errors.New("no primary account for vacation response")
errNoPrimaryAccountForSubmission = errors.New("no primary account for submission")
+ errNoPrimaryAccountForTask = errors.New("no primary account for task")
+ errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
+ errNoPrimaryAccountForContact = errors.New("no primary account for contact")
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
// errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
@@ -105,6 +108,18 @@ func (r Request) GetAccountIdForSubmission() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission)
}
+func (r Request) GetAccountIdForTask() (string, *Error) {
+ return r.getAccountId(r.session.PrimaryAccounts.Task, errNoPrimaryAccountForTask)
+}
+
+func (r Request) GetAccountIdForCalendar() (string, *Error) {
+ return r.getAccountId(r.session.PrimaryAccounts.Calendar, errNoPrimaryAccountForCalendar)
+}
+
+func (r Request) GetAccountIdForContact() (string, *Error) {
+ return r.getAccountId(r.session.PrimaryAccounts.Contact, errNoPrimaryAccountForContact)
+}
+
func (r Request) GetAccountForMail() (jmap.Account, *Error) {
accountId, err := r.GetAccountIdForMail()
if err != nil {
@@ -277,3 +292,99 @@ func (r Request) observeJmapError(jerr jmap.Error) jmap.Error {
}
return jerr
}
+
+func (r Request) needTask() (bool, Response) {
+ if r.session.Capabilities.Tasks == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingTasksSessionCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needTaskForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needTask(); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorAccountNotFound), r.session.State)
+ }
+ if account.AccountCapabilities.Tasks == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingTasksAccountCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needTaskWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForTask()
+ if err != nil {
+ return false, "", errorResponse(err)
+ }
+ if ok, resp := r.needTaskForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
+
+func (r Request) needCalendar() (bool, Response) {
+ if r.session.Capabilities.Calendars == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needCalendarForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needCalendar(); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorAccountNotFound), r.session.State)
+ }
+ if account.AccountCapabilities.Calendars == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needCalendarWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForCalendar()
+ if err != nil {
+ return false, "", errorResponse(err)
+ }
+ if ok, resp := r.needCalendarForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
+
+func (r Request) needContact() (bool, Response) {
+ if r.session.Capabilities.Contacts == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsSessionCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needContactForAccount(accountId string) (bool, Response) {
+ if ok, resp := r.needContact(); !ok {
+ return ok, resp
+ }
+ account, ok := r.session.Accounts[accountId]
+ if !ok {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorAccountNotFound), r.session.State)
+ }
+ if account.AccountCapabilities.Contacts == nil {
+ return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsAccountCapability), r.session.State)
+ }
+ return true, Response{}
+}
+
+func (r Request) needContactWithAccount() (bool, string, Response) {
+ accountId, err := r.GetAccountIdForContact()
+ if err != nil {
+ return false, "", errorResponse(err)
+ }
+ if ok, resp := r.needContactForAccount(accountId); !ok {
+ return false, accountId, resp
+ }
+ return true, accountId, Response{}
+}
diff --git a/services/groupware/pkg/groupware/groupware_response.go b/services/groupware/pkg/groupware/groupware_response.go
index c8abb3222f..d380fa9e4b 100644
--- a/services/groupware/pkg/groupware/groupware_response.go
+++ b/services/groupware/pkg/groupware/groupware_response.go
@@ -23,6 +23,15 @@ func errorResponse(err *Error) Response {
}
}
+func errorResponseWithSessionState(err *Error, sessionState jmap.SessionState) Response {
+ return Response{
+ body: nil,
+ err: err,
+ etag: "",
+ sessionState: sessionState,
+ }
+}
+
func response(body any, sessionState jmap.SessionState) Response {
return Response{
body: body,