From eb2660a631dcbd334c15dc2d89fb74539a3da685 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 10 Sep 2025 17:01:57 +0200
Subject: [PATCH] fix(groupware): fix JMAP error handling
* the JMAP error handling was not working properly, fixed it and added
error definitions accordingly
* add operations to retrieve mailbox roles and mailboxes by role for
all accounts
---
pkg/jmap/jmap_api_mailbox.go | 144 +++++++++++++-----
pkg/jmap/jmap_error.go | 11 +-
pkg/jmap/jmap_model.go | 77 +++++++++-
pkg/jmap/jmap_tools.go | 53 ++++++-
pkg/jmap/jmap_tools_test.go | 16 ++
.../pkg/groupware/groupware_api_mailbox.go | 42 ++++-
.../pkg/groupware/groupware_error.go | 63 ++++++++
.../pkg/groupware/groupware_route.go | 5 +-
8 files changed, 357 insertions(+), 54 deletions(-)
diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go
index 633499e19f..6c43e20b65 100644
--- a/pkg/jmap/jmap_api_mailbox.go
+++ b/pkg/jmap/jmap_api_mailbox.go
@@ -2,6 +2,7 @@ package jmap
import (
"context"
+ "slices"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
@@ -15,41 +16,27 @@ type MailboxesResponse struct {
}
// https://jmap.io/spec-mail.html#mailboxget
-func (j *Client) GetMailbox(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (map[string]MailboxesResponse, SessionState, Error) {
+func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, SessionState, Error) {
logger = j.logger("GetMailbox", session, logger)
- uniqueAccountIds := structs.Uniq(accountIds)
- n := len(uniqueAccountIds)
- if n < 1 {
- return map[string]MailboxesResponse{}, "", nil
- }
-
- invocations := make([]Invocation, n)
- for i, accountId := range uniqueAccountIds {
- invocations[i] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, mcid(accountId, "0"))
- }
-
- cmd, err := j.request(session, logger, invocations...)
+ cmd, err := j.request(session, logger,
+ invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, "0"),
+ )
if err != nil {
- return map[string]MailboxesResponse{}, "", err
+ return MailboxesResponse{}, "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]MailboxesResponse, Error) {
- resp := map[string]MailboxesResponse{}
- for _, accountId := range uniqueAccountIds {
- var response MailboxGetResponse
- err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "0"), &response)
- if err != nil {
- return map[string]MailboxesResponse{}, err
- }
-
- resp[accountId] = MailboxesResponse{
- Mailboxes: response.List,
- NotFound: response.NotFound,
- State: response.State,
- }
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxesResponse, Error) {
+ var response MailboxGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "0", &response)
+ if err != nil {
+ return MailboxesResponse{}, err
}
- return resp, nil
+ return MailboxesResponse{
+ Mailboxes: response.List,
+ NotFound: response.NotFound,
+ State: response.State,
+ }, nil
})
}
@@ -59,20 +46,40 @@ type AllMailboxesResponse struct {
}
func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (map[string]AllMailboxesResponse, SessionState, Error) {
- resp, sessionState, err := j.GetMailbox(accountIds, session, ctx, logger, nil)
+ logger = j.logger("GetAllMailboxes", session, logger)
+
+ uniqueAccountIds := structs.Uniq(accountIds)
+ n := len(uniqueAccountIds)
+ if n < 1 {
+ return map[string]AllMailboxesResponse{}, "", nil
+ }
+
+ invocations := make([]Invocation, n)
+ for i, accountId := range uniqueAccountIds {
+ invocations[i] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId}, mcid(accountId, "0"))
+ }
+
+ cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return map[string]AllMailboxesResponse{}, sessionState, err
+ return map[string]AllMailboxesResponse{}, "", err
}
- mapped := make(map[string]AllMailboxesResponse, len(resp))
- for accountId, mailboxesResponse := range resp {
- mapped[accountId] = AllMailboxesResponse{
- Mailboxes: mailboxesResponse.Mailboxes,
- State: mailboxesResponse.State,
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]AllMailboxesResponse, Error) {
+ resp := map[string]AllMailboxesResponse{}
+ for _, accountId := range uniqueAccountIds {
+ var response MailboxGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "0"), &response)
+ if err != nil {
+ return map[string]AllMailboxesResponse{}, err
+ }
+
+ resp[accountId] = AllMailboxesResponse{
+ Mailboxes: response.List,
+ State: response.State,
+ }
}
- }
-
- return mapped, sessionState, nil
+ return resp, nil
+ })
}
type Mailboxes struct {
@@ -92,7 +99,11 @@ func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx cont
invocations[i*2+0] = invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: filter}, mcid(accountId, "0"))
invocations[i*2+1] = invocation(CommandMailboxGet, MailboxGetRefCommand{
AccountId: accountId,
- IdRef: &ResultReference{Name: CommandMailboxQuery, Path: "/ids/*", ResultOf: mcid(accountId, "0")},
+ IdsRef: &ResultReference{
+ Name: CommandMailboxQuery,
+ Path: "/ids/*",
+ ResultOf: mcid(accountId, "0"),
+ },
}, mcid(accountId, "1"))
}
cmd, err := j.request(session, logger, invocations...)
@@ -288,3 +299,56 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi
return resp, nil
})
}
+
+func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (map[string][]string, SessionState, Error) {
+ logger = j.logger("GetMailboxRolesForMultipleAccounts", session, logger)
+
+ uniqueAccountIds := structs.Uniq(accountIds)
+ n := len(uniqueAccountIds)
+ if n < 1 {
+ return map[string][]string{}, "", nil
+ }
+
+ t := true
+
+ invocations := make([]Invocation, n*2)
+ for i, accountId := range uniqueAccountIds {
+ invocations[i*2+0] = invocation(CommandMailboxQuery, MailboxQueryCommand{
+ AccountId: accountId,
+ Filter: MailboxFilterCondition{
+ HasAnyRole: &t,
+ },
+ }, mcid(accountId, "0"))
+ invocations[i*2+1] = invocation(CommandMailboxGet, MailboxGetRefCommand{
+ AccountId: accountId,
+ IdsRef: &ResultReference{
+ ResultOf: mcid(accountId, "0"),
+ Name: CommandMailboxQuery,
+ Path: "/ids",
+ },
+ }, mcid(accountId, "1"))
+ }
+
+ cmd, err := j.request(session, logger, invocations...)
+ if err != nil {
+ return map[string][]string{}, "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string][]string, Error) {
+ resp := make(map[string][]string, n)
+ for _, accountId := range uniqueAccountIds {
+ var getResponse MailboxGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &getResponse)
+ if err != nil {
+ return map[string][]string{}, err
+ }
+ roles := make([]string, len(getResponse.List))
+ for i, mailbox := range getResponse.List {
+ roles[i] = mailbox.Role
+ }
+ slices.Sort(roles)
+ resp[accountId] = roles
+ }
+ return resp, nil
+ })
+}
diff --git a/pkg/jmap/jmap_error.go b/pkg/jmap/jmap_error.go
index bbb49b520d..710ff397bb 100644
--- a/pkg/jmap/jmap_error.go
+++ b/pkg/jmap/jmap_error.go
@@ -18,9 +18,18 @@ const (
JmapErrorInvalidSessionResponse
JmapErrorInvalidJmapRequestPayload
JmapErrorInvalidJmapResponsePayload
- JmapErrorMethodLevel
JmapErrorSetError
JmapErrorTooManyMethodCalls
+ JmapErrorUnspecifiedType
+ JmapErrorServerUnavailable
+ JmapErrorServerFail
+ JmapErrorUnknownMethod
+ JmapErrorInvalidArguments
+ JmapErrorInvalidResultReference
+ JmapErrorForbidden
+ JmapErrorAccountNotFound
+ JmapErrorAccountNotSupportedByMethod
+ JmapErrorAccountReadOnly
)
var (
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 9ce4552005..48dbd50746 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -27,6 +27,28 @@ const (
JmapKeywordJunk = "$junk"
JmapKeywordNotJunk = "$notjunk"
JmapKeywordMdnSent = "$mdnsent"
+
+ // https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml
+ //JmapMailboxRoleAll = "all"
+ //JmapMailboxRoleArchive = "archive"
+ JmapMailboxRoleDrafts = "drafts"
+ //JmapMailboxRoleFlagged = "flagged"
+ //JmapMailboxRoleImportant = "important"
+ JmapMailboxRoleInbox = "inbox"
+ JmapMailboxRoleJunk = "junk"
+ JmapMailboxRoleSent = "sent"
+ //JmapMailboxRoleSubscribed = "subscribed"
+ JmapMailboxRoleTrash = "trash"
+)
+
+var (
+ JmapMailboxRoles = []string{
+ JmapMailboxRoleInbox,
+ JmapMailboxRoleSent,
+ JmapMailboxRoleDrafts,
+ JmapMailboxRoleJunk,
+ JmapMailboxRoleTrash,
+ }
)
type SessionMailAccountCapabilities struct {
@@ -352,6 +374,52 @@ type SessionResponse struct {
State SessionState `json:"state,omitempty"`
}
+// Method level error types.
+const (
+ // Some internal server resource was temporarily unavailable.
+ //
+ // Attempting the same operation later (perhaps after a backoff with a random factor) may succeed.
+ MethodLevelErrorServerUnavailable = "serverUnavailable"
+
+ // An unexpected or unknown error occurred during the processing of the call.
+ //
+ // A description property should provide more details about the error. The method call made no changes
+ // to the server’s state. Attempting the same operation again is expected to fail again.
+ // Contacting the service administrator is likely necessary to resolve this problem if it is persistent.
+ MethodLevelErrorServerFail = "serverFail"
+
+ // Some, but not all, expected changes described by the method occurred.
+ //
+ // The client MUST resynchronise impacted data to determine server state. Use of this error is strongly discouraged.
+ MethodLevelErrorServerPartialFail = "serverPartialFail"
+
+ // The server does not recognise this method name.
+ MethodLevelErrorUnknownMethod = "unknownMethod"
+
+ // One of the arguments is of the wrong type or is otherwise invalid, or a required argument is missing.
+ //
+ // A description property MAY be present to help debug with an explanation of what the problem was.
+ // This is a non-localised string, and it is not intended to be shown directly to end users.
+ MethodLevelErrorInvalidArguments = "invalidArguments"
+
+ // The method used a result reference for one of its arguments, but this failed to resolve.
+ MethodLevelErrorInvalidResultReference = "invalidResultReference"
+
+ // The method and arguments are valid, but executing the method would violate an Access Control List
+ // (ACL) or other permissions policy.
+ MethodLevelErrorForbidden = "forbidden"
+
+ // The accountId does not correspond to a valid account.
+ MethodLevelErrorAccountNotFound = "accountNotFound"
+
+ // The accountId given corresponds to a valid account, but the account does not support this method or data type.
+ MethodLevelErrorAccountNotSupportedByMethod = "accountNotSupportedByMethod"
+
+ // This method modifies state, but the account is read-only (as returned on the corresponding Account object in
+ // the JMAP Session resource).
+ MethodLevelErrorAccountReadOnly = "accountReadOnly"
+)
+
// SetError type values.
const (
// The create/update/destroy would violate an ACL or other permissions policy.
@@ -648,7 +716,7 @@ type MailboxGetCommand struct {
type MailboxGetRefCommand struct {
AccountId string `json:"accountId"`
- IdRef *ResultReference `json:"#ids,omitempty"`
+ IdsRef *ResultReference `json:"#ids,omitempty"`
}
type MailboxChangesCommand struct {
@@ -2654,7 +2722,13 @@ type SearchSnippetGetResponse struct {
NotFound []string `json:"notFound,omitempty"`
}
+type ErrorResponse struct {
+ Type string `json:"type"`
+ Description string `json:"description,omitempty"`
+}
+
const (
+ ErrorCommand Command = "error" // only occurs in responses
CommandBlobGet Command = "Blob/get"
CommandBlobUpload Command = "Blob/upload"
CommandEmailGet Command = "Email/get"
@@ -2675,6 +2749,7 @@ const (
)
var CommandResponseTypeMap = map[Command]func() any{
+ ErrorCommand: func() any { return ErrorResponse{} },
CommandBlobGet: func() any { return BlobGetResponse{} },
CommandBlobUpload: func() any { return BlobUploadResponse{} },
CommandMailboxQuery: func() any { return MailboxQueryResponse{} },
diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go
index 81feedfaa8..4db5c4709c 100644
--- a/pkg/jmap/jmap_tools.go
+++ b/pkg/jmap/jmap_tools.go
@@ -3,9 +3,11 @@ package jmap
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"maps"
"reflect"
+ "strings"
"sync"
"time"
@@ -66,7 +68,7 @@ func command[T any](api ApiClient,
var response Response
err := json.Unmarshal(responseBody, &response)
if err != nil {
- logger.Error().Err(err).Msg("failed to deserialize body JSON payload")
+ logger.Error().Err(err).Msgf("failed to deserialize body JSON payload into a %T", response)
var zero T
return zero, "", SimpleError{code: JmapErrorDecodingResponseBody, err: err}
}
@@ -80,15 +82,50 @@ func command[T any](api ApiClient,
// search for an "error" response
// https://jmap.io/spec-core.html#method-level-errors
for _, mr := range response.MethodResponses {
- if mr.Command == "error" {
- err := fmt.Errorf("found method level error in response '%v'", mr.Tag)
- if payload, ok := mr.Parameters.(map[string]any); ok {
- if errorType, ok := payload["type"]; ok {
- err = fmt.Errorf("found method level error in response '%v', type: '%v'", mr.Tag, errorType)
+ if mr.Command == ErrorCommand {
+ if errorParameters, ok := mr.Parameters.(ErrorResponse); ok {
+ code := JmapErrorServerFail
+ switch errorParameters.Type {
+ case MethodLevelErrorServerUnavailable:
+ code = JmapErrorServerUnavailable
+ case MethodLevelErrorServerFail, MethodLevelErrorServerPartialFail:
+ code = JmapErrorServerFail
+ case MethodLevelErrorUnknownMethod:
+ code = JmapErrorUnknownMethod
+ case MethodLevelErrorInvalidArguments:
+ code = JmapErrorInvalidArguments
+ case MethodLevelErrorInvalidResultReference:
+ code = JmapErrorInvalidResultReference
+ case MethodLevelErrorForbidden:
+ // there's a quirk here: when referencing an account that exists but that this
+ // user has no access to, Stalwart returns the 'forbidden' error, but this might
+ // leak the existence of an account to an attacker -- instead, we deem it safer to
+ // return a "account does not exist" error instead
+ if strings.HasPrefix(errorParameters.Description, "You do not have access to account") {
+ code = JmapErrorAccountNotFound
+ } else {
+ code = JmapErrorForbidden
+ }
+ case MethodLevelErrorAccountNotFound:
+ code = JmapErrorAccountNotFound
+ case MethodLevelErrorAccountNotSupportedByMethod:
+ code = JmapErrorAccountNotSupportedByMethod
+ case MethodLevelErrorAccountReadOnly:
+ code = JmapErrorAccountReadOnly
}
+ msg := fmt.Sprintf("found method level error in response '%v', type: '%v', description: '%v'", mr.Tag, errorParameters.Type, errorParameters.Description)
+ err = errors.New(msg)
+ logger.Warn().Int("code", code).Str("type", errorParameters.Type).Msg(msg)
+ var zero T
+ return zero, response.SessionState, SimpleError{code: code, err: err}
+ } else {
+ code := JmapErrorUnspecifiedType
+ msg := fmt.Sprintf("found method level error in response '%v'", mr.Tag)
+ err := errors.New(msg)
+ logger.Warn().Int("code", code).Msg(msg)
+ var zero T
+ return zero, response.SessionState, SimpleError{code: code, err: err}
}
- var zero T
- return zero, response.SessionState, SimpleError{code: JmapErrorMethodLevel, err: err}
}
}
diff --git a/pkg/jmap/jmap_tools_test.go b/pkg/jmap/jmap_tools_test.go
index d6c385b770..2c0beea6d8 100644
--- a/pkg/jmap/jmap_tools_test.go
+++ b/pkg/jmap/jmap_tools_test.go
@@ -134,3 +134,19 @@ func TestMarshallingUnknown(t *testing.T) {
require.NoError(err)
require.Equal(`{"subject":"aaa","bodyStructure":{"header:a":"bc","header:x":"yz","partId":"b","type":"a"}}`, string(result))
}
+
+func TestUnmarshallingError(t *testing.T) {
+ require := require.New(t)
+
+ responseBody := `{"methodResponses":[["error",{"type":"forbidden","description":"You do not have access to account a"},"a:0"]],"sessionState":"3e25b2a0"}`
+ var response Response
+ err := json.Unmarshal([]byte(responseBody), &response)
+ require.NoError(err)
+ require.Len(response.MethodResponses, 1)
+ require.Equal(ErrorCommand, response.MethodResponses[0].Command)
+ require.Equal("a:0", response.MethodResponses[0].Tag)
+ require.IsType(ErrorResponse{}, response.MethodResponses[0].Parameters)
+ er, _ := response.MethodResponses[0].Parameters.(ErrorResponse)
+ require.Equal("forbidden", er.Type)
+ require.Equal("You do not have access to account a", er.Description)
+}
diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go
index 75ad42634d..2472e6a21a 100644
--- a/services/groupware/pkg/groupware/groupware_api_mailbox.go
+++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go
@@ -41,13 +41,12 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
- mailboxesByAccountId, sessionState, jerr := g.jmap.GetMailbox([]string{accountId}, req.session, req.ctx, req.logger, []string{mailboxId})
+ mailboxes, sessionState, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, []string{mailboxId})
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- mailboxes, ok := mailboxesByAccountId[accountId]
- if ok && len(mailboxes.Mailboxes) == 1 {
+ if len(mailboxes.Mailboxes) == 1 {
return etagResponse(mailboxes.Mailboxes[0], sessionState, mailboxes.State)
} else {
return notFoundResponse(sessionState)
@@ -209,6 +208,27 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
})
}
+func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *http.Request) {
+ role := chi.URLParam(r, UriParamRole)
+ g.respond(w, r, func(req Request) Response {
+ accountIds := structs.Keys(req.session.Accounts)
+ if len(accountIds) < 1 {
+ return noContentResponse("")
+ }
+ logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)).Str("role", role))
+
+ filter := jmap.MailboxFilterCondition{
+ Role: role,
+ }
+
+ mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, filter)
+ if err != nil {
+ return req.errorResponseFromJmap(err)
+ }
+ return response(mailboxesByAccountId, sessionState)
+ })
+}
+
// When the request succeeds.
// swagger:response MailboxChangesResponse200
type SwaggerMailboxChangesResponse200 struct {
@@ -308,3 +328,19 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
return response(changesByAccountId, sessionState)
})
}
+
+func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With()
+ allAccountIds := structs.Keys(req.session.Accounts) // TODO(pbleser-oc) do we need a limit for a maximum amount of accounts to query at once?
+ l.Array(logAccountId, log.SafeStringArray(allAccountIds))
+ logger := log.From(l)
+
+ rolesByAccountId, sessionState, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return response(rolesByAccountId, sessionState)
+ })
+}
diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go
index ae95a0a3f4..0039cb7fd7 100644
--- a/services/groupware/pkg/groupware/groupware_error.go
+++ b/services/groupware/pkg/groupware/groupware_error.go
@@ -138,6 +138,20 @@ func groupwareErrorFromJmap(j jmap.Error) *GroupwareError {
return &ErrorInvalidRequestPayload
case jmap.JmapErrorInvalidJmapResponsePayload:
return &ErrorInvalidResponsePayload
+ case jmap.JmapErrorUnspecifiedType, jmap.JmapErrorUnknownMethod, jmap.JmapErrorInvalidArguments, jmap.JmapErrorInvalidResultReference:
+ return &ErrorInvalidGroupwareRequest
+ case jmap.JmapErrorServerUnavailable:
+ return &ErrorServerUnavailable
+ case jmap.JmapErrorServerFail:
+ return &ErrorServerFailure
+ case jmap.JmapErrorForbidden:
+ return &ErrorForbiddenOperation
+ case jmap.JmapErrorAccountNotFound:
+ return &ErrorAccountNotFound
+ case jmap.JmapErrorAccountNotSupportedByMethod:
+ return &ErrorAccountNotSupportedByMethod
+ case jmap.JmapErrorAccountReadOnly:
+ return &ErrorAccountReadOnly
default:
return &ErrorGeneric
}
@@ -167,6 +181,13 @@ const (
ErrorCodeInvalidUserRequest = "INVURQ"
ErrorCodeUsernameEmailDomainNotGreenListed = "UEDGRE"
ErrorCodeUsernameEmailDomainRedListed = "UEDRED"
+ ErrorCodeInvalidGroupwareRequest = "GPRERR"
+ ErrorCodeServerUnavailable = "SRVUNA"
+ ErrorCodeServerFailure = "SRVFLR"
+ ErrorCodeForbiddenOperation = "FRBOPR"
+ ErrorCodeAccountNotFound = "ACCNFD"
+ ErrorCodeAccountNotSupportedByMethod = "ACCNSM"
+ ErrorCodeAccountReadOnly = "ACCRDO"
)
var (
@@ -308,6 +329,48 @@ var (
Title: "Domain is redlisted",
Detail: "The username email address domain is redlisted.",
}
+ ErrorInvalidGroupwareRequest = GroupwareError{
+ Status: http.StatusInternalServerError,
+ Code: ErrorCodeInvalidGroupwareRequest,
+ Title: "Internal Request Error",
+ Detail: "The request constructed by the Groupware is regarded as invalid by the Mail server.",
+ }
+ ErrorServerUnavailable = GroupwareError{
+ Status: http.StatusServiceUnavailable,
+ Code: ErrorCodeServerUnavailable,
+ Title: "Mail Server is unavailable",
+ Detail: "The Mail Server is currently unable to process the request.",
+ }
+ ErrorServerFailure = GroupwareError{
+ Status: http.StatusInternalServerError,
+ Code: ErrorCodeServerFailure,
+ Title: "Mail Server is unable to process the Request",
+ Detail: "The Mail Server is unable to process the request.",
+ }
+ ErrorForbiddenOperation = GroupwareError{
+ Status: http.StatusForbidden,
+ Code: ErrorCodeForbiddenOperation,
+ Title: "The Operation is forbidden by the Mail Server",
+ Detail: "The Mail Server refuses to perform the request.",
+ }
+ ErrorAccountNotFound = GroupwareError{
+ Status: http.StatusNotFound,
+ Code: ErrorCodeAccountNotFound,
+ Title: "The referenced Account does not exist",
+ Detail: "The Account that was referenced in the request does not exist.",
+ }
+ ErrorAccountNotSupportedByMethod = GroupwareError{
+ Status: http.StatusForbidden,
+ Code: ErrorCodeAccountNotSupportedByMethod,
+ Title: "The referenced Account does not supported the requested method",
+ Detail: "The Account that was referenced in the request does not supported the requested method or data type.",
+ }
+ ErrorAccountReadOnly = GroupwareError{
+ Status: http.StatusForbidden,
+ Code: ErrorCodeAccountReadOnly,
+ Title: "The referenced Account is read-only",
+ Detail: "The Account that was referenced in the request only supports read-only operations.",
+ }
)
type ErrorOpt interface {
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 664fb61e99..b87126b38a 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -15,6 +15,7 @@ const (
UriParamBlobId = "blobid"
UriParamBlobName = "blobname"
UriParamStreamId = "stream"
+ UriParamRole = "role"
QueryParamMailboxSearchName = "name"
QueryParamMailboxSearchRole = "role"
QueryParamMailboxSearchSubscribed = "subscribed"
@@ -48,8 +49,10 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/accounts", g.GetAccounts)
r.Route("/accounts/all", func(r chi.Router) {
r.Route("/mailboxes", func(r chi.Router) {
- r.Get("/", g.GetMailboxesForAllAccounts)
+ r.Get("/", g.GetMailboxesForAllAccounts) // ?role=
r.Get("/changes", g.GetMailboxChangesForAllAccounts)
+ r.Get("/roles", g.GetMailboxRoles) // ?role=
+ r.Get("/roles/{role}", g.GetMailboxByRoleForAllAccounts) // ?role=
})
})
r.Route("/accounts/{accountid}", func(r chi.Router) {