groupware: add JMAP capability checking (in part: for contacts, calendars, tasks)

This commit is contained in:
Pascal Bleser
2025-10-02 17:02:52 +02:00
parent abf9549082
commit 0cebbbd83f
8 changed files with 459 additions and 45 deletions

View File

@@ -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

View File

@@ -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,
},
},

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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{}
}

View File

@@ -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,