mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-16 00:59:37 -06:00
groupware: implement JSON:API's error response format, with a revamped error handling in jmap and services/groupware
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
wellKnown WellKnownClient
|
||||
wellKnown SessionClient
|
||||
api ApiClient
|
||||
io.Closer
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func (j *Client) Close() error {
|
||||
return j.api.Close()
|
||||
}
|
||||
|
||||
func NewClient(wellKnown WellKnownClient, api ApiClient) Client {
|
||||
func NewClient(wellKnown SessionClient, api ApiClient) Client {
|
||||
return Client{
|
||||
wellKnown: wellKnown,
|
||||
api: api,
|
||||
@@ -79,41 +79,23 @@ func (s Session) DecorateLogger(l log.Logger) log.Logger {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
errWellKnownResponseHasNoUsername = fmt.Errorf("well-known response has no username")
|
||||
errWellKnownResponseHasJmapMailPrimaryAccount = fmt.Errorf("PrimaryAccounts in well-known response has no entry for %v", JmapMail)
|
||||
errWellKnownResponseHasNoApiUrl = fmt.Errorf("well-known response has no API URL")
|
||||
)
|
||||
|
||||
type WellKnownResponseHasInvalidApiUrlError struct {
|
||||
ApiUrl string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e WellKnownResponseHasInvalidApiUrlError) Error() string {
|
||||
return fmt.Sprintf("well-known response contains an invalid API URL '%s': %v", e.ApiUrl, e.Err.Error())
|
||||
}
|
||||
func (e WellKnownResponseHasInvalidApiUrlError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Create a new Session from a WellKnownResponse.
|
||||
func NewSession(wellKnownResponse WellKnownResponse) (Session, error) {
|
||||
username := wellKnownResponse.Username
|
||||
func NewSession(sessionResponse SessionResponse) (Session, Error) {
|
||||
username := sessionResponse.Username
|
||||
if username == "" {
|
||||
return Session{}, errWellKnownResponseHasNoUsername
|
||||
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide a username")}
|
||||
}
|
||||
accountId := wellKnownResponse.PrimaryAccounts[JmapMail]
|
||||
accountId := sessionResponse.PrimaryAccounts[JmapMail]
|
||||
if accountId == "" {
|
||||
return Session{}, errWellKnownResponseHasJmapMailPrimaryAccount
|
||||
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide a primary mail account")}
|
||||
}
|
||||
apiStr := wellKnownResponse.ApiUrl
|
||||
apiStr := sessionResponse.ApiUrl
|
||||
if apiStr == "" {
|
||||
return Session{}, errWellKnownResponseHasNoApiUrl
|
||||
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide an API URL")}
|
||||
}
|
||||
apiUrl, err := url.Parse(apiStr)
|
||||
if err != nil {
|
||||
return Session{}, WellKnownResponseHasInvalidApiUrlError{ApiUrl: apiStr, Err: err}
|
||||
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response provides an invalid API URL")}
|
||||
}
|
||||
return Session{
|
||||
Username: username,
|
||||
@@ -123,8 +105,8 @@ func NewSession(wellKnownResponse WellKnownResponse) (Session, error) {
|
||||
}
|
||||
|
||||
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
|
||||
func (j *Client) FetchSession(username string, logger *log.Logger) (Session, error) {
|
||||
wk, err := j.wellKnown.GetWellKnown(username, logger)
|
||||
func (j *Client) FetchSession(username string, logger *log.Logger) (Session, Error) {
|
||||
wk, err := j.wellKnown.GetSession(username, logger)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
@@ -141,62 +123,62 @@ func (j *Client) loggerParams(operation string, session *Session, logger *log.Lo
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#identityget
|
||||
func (j *Client) GetIdentity(session *Session, ctx context.Context, logger *log.Logger) (IdentityGetResponse, error) {
|
||||
func (j *Client) GetIdentity(session *Session, ctx context.Context, logger *log.Logger) (IdentityGetResponse, Error) {
|
||||
logger = j.logger("GetIdentity", session, logger)
|
||||
cmd, err := request(invocation(IdentityGet, IdentityGetCommand{AccountId: session.AccountId}, "0"))
|
||||
if err != nil {
|
||||
return IdentityGetResponse{}, err
|
||||
return IdentityGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (IdentityGetResponse, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (IdentityGetResponse, Error) {
|
||||
var response IdentityGetResponse
|
||||
err = retrieveResponseMatchParameters(body, IdentityGet, "0", &response)
|
||||
return response, err
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#vacationresponseget
|
||||
func (j *Client) GetVacationResponse(session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, error) {
|
||||
func (j *Client) GetVacationResponse(session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, Error) {
|
||||
logger = j.logger("GetVacationResponse", session, logger)
|
||||
cmd, err := request(invocation(VacationResponseGet, VacationResponseGetCommand{AccountId: session.AccountId}, "0"))
|
||||
if err != nil {
|
||||
return VacationResponseGetResponse{}, err
|
||||
return VacationResponseGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (VacationResponseGetResponse, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (VacationResponseGetResponse, Error) {
|
||||
var response VacationResponseGetResponse
|
||||
err = retrieveResponseMatchParameters(body, VacationResponseGet, "0", &response)
|
||||
return response, err
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxget
|
||||
func (j *Client) GetMailbox(session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxGetResponse, error) {
|
||||
func (j *Client) GetMailbox(session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxGetResponse, Error) {
|
||||
logger = j.logger("GetMailbox", session, logger)
|
||||
cmd, err := request(invocation(MailboxGet, MailboxGetCommand{AccountId: session.AccountId, Ids: ids}, "0"))
|
||||
if err != nil {
|
||||
return MailboxGetResponse{}, err
|
||||
return MailboxGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (MailboxGetResponse, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (MailboxGetResponse, Error) {
|
||||
var response MailboxGetResponse
|
||||
err = retrieveResponseMatchParameters(body, MailboxGet, "0", &response)
|
||||
return response, err
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) GetAllMailboxes(session *Session, ctx context.Context, logger *log.Logger) (MailboxGetResponse, error) {
|
||||
func (j *Client) GetAllMailboxes(session *Session, ctx context.Context, logger *log.Logger) (MailboxGetResponse, Error) {
|
||||
return j.GetMailbox(session, ctx, logger, nil)
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxquery
|
||||
func (j *Client) QueryMailbox(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (MailboxQueryResponse, error) {
|
||||
func (j *Client) QueryMailbox(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (MailboxQueryResponse, Error) {
|
||||
logger = j.logger("QueryMailbox", session, logger)
|
||||
cmd, err := request(invocation(MailboxQuery, SimpleMailboxQueryCommand{AccountId: session.AccountId, Filter: filter}, "0"))
|
||||
if err != nil {
|
||||
return MailboxQueryResponse{}, err
|
||||
return MailboxQueryResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (MailboxQueryResponse, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (MailboxQueryResponse, Error) {
|
||||
var response MailboxQueryResponse
|
||||
err = retrieveResponseMatchParameters(body, MailboxQuery, "0", &response)
|
||||
return response, err
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,7 +187,7 @@ type Mailboxes struct {
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) SearchMailboxes(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (Mailboxes, error) {
|
||||
func (j *Client) SearchMailboxes(session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterCondition) (Mailboxes, Error) {
|
||||
logger = j.logger("SearchMailboxes", session, logger)
|
||||
|
||||
cmd, err := request(
|
||||
@@ -216,14 +198,14 @@ func (j *Client) SearchMailboxes(session *Session, ctx context.Context, logger *
|
||||
}, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
return Mailboxes{}, err
|
||||
return Mailboxes{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (Mailboxes, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (Mailboxes, Error) {
|
||||
var response MailboxGetResponse
|
||||
err = retrieveResponseMatchParameters(body, MailboxGet, "1", &response)
|
||||
if err != nil {
|
||||
return Mailboxes{}, err
|
||||
return Mailboxes{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
return Mailboxes{Mailboxes: response.List, State: body.SessionState}, nil
|
||||
})
|
||||
@@ -234,7 +216,7 @@ type Emails struct {
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) GetEmails(session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (Emails, error) {
|
||||
func (j *Client) GetEmails(session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (Emails, Error) {
|
||||
logger = j.loggerParams("GetEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Int(logLimit, limit)
|
||||
})
|
||||
@@ -267,14 +249,14 @@ func (j *Client) GetEmails(session *Session, ctx context.Context, logger *log.Lo
|
||||
invocation(EmailGet, get, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
return Emails{}, err
|
||||
return Emails{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (Emails, error) {
|
||||
return command(j.api, logger, ctx, session, cmd, func(body *Response) (Emails, Error) {
|
||||
var response EmailGetResponse
|
||||
err = retrieveResponseMatchParameters(body, EmailGet, "1", &response)
|
||||
if err != nil {
|
||||
return Emails{}, err
|
||||
return Emails{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
return Emails{Emails: response.List, State: body.SessionState}, nil
|
||||
})
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type ApiClient interface {
|
||||
Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, error)
|
||||
Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error)
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type WellKnownClient interface {
|
||||
GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error)
|
||||
type SessionClient interface {
|
||||
GetSession(username string, logger *log.Logger) (SessionResponse, Error)
|
||||
}
|
||||
|
||||
45
pkg/jmap/jmap_error.go
Normal file
45
pkg/jmap/jmap_error.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package jmap
|
||||
|
||||
const (
|
||||
JmapErrorAuthenticationFailed = iota
|
||||
JmapErrorInvalidHttpRequest
|
||||
JmapErrorServerResponse
|
||||
JmapErrorReadingResponseBody
|
||||
JmapErrorDecodingResponseBody
|
||||
JmapErrorEncodingRequestBody
|
||||
JmapErrorCreatingRequest
|
||||
JmapErrorSendingRequest
|
||||
JmapErrorInvalidSessionResponse
|
||||
JmapErrorInvalidJmapRequestPayload
|
||||
JmapErrorInvalidJmapResponsePayload
|
||||
)
|
||||
|
||||
type Error interface {
|
||||
Code() int
|
||||
error
|
||||
}
|
||||
|
||||
type SimpleError struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
|
||||
var _ Error = &SimpleError{}
|
||||
|
||||
func (e SimpleError) Code() int {
|
||||
return e.code
|
||||
}
|
||||
func (e SimpleError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
func (e SimpleError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func simpleError(err error, code int) Error {
|
||||
if err != nil {
|
||||
return SimpleError{code: code, err: err}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ type HttpJmapApiClient struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ ApiClient = &HttpJmapApiClient{}
|
||||
_ WellKnownClient = &HttpJmapApiClient{}
|
||||
_ ApiClient = &HttpJmapApiClient{}
|
||||
_ SessionClient = &HttpJmapApiClient{}
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -64,28 +64,13 @@ func (h *HttpJmapApiClient) auth(username string, logger *log.Logger, req *http.
|
||||
return nil
|
||||
}
|
||||
|
||||
type HttpError struct {
|
||||
Method string
|
||||
Url string
|
||||
Username string
|
||||
Op string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e HttpError) Error() string {
|
||||
return fmt.Sprintf("HTTP error for method=%v url='%v' username='%v' while %v: %v", e.Method, e.Url, e.Username, e.Op, e.Err.Error())
|
||||
}
|
||||
func (e HttpError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) {
|
||||
func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
wellKnownUrl := h.baseurl.JoinPath(".well-known", "jmap").String()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create GET request for %v", wellKnownUrl)
|
||||
return WellKnownResponse{}, HttpError{Op: "creating request", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: err}
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
h.auth(username, logger, req)
|
||||
req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation
|
||||
@@ -93,11 +78,11 @@ func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (W
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", wellKnownUrl)
|
||||
return WellKnownResponse{}, HttpError{Op: "performing request", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: err}
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 200")
|
||||
return WellKnownResponse{}, HttpError{Op: "processing response", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: fmt.Errorf("status is %v", res.Status)}
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
|
||||
}
|
||||
if res.Body != nil {
|
||||
defer func(Body io.ReadCloser) {
|
||||
@@ -111,32 +96,32 @@ func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (W
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
return WellKnownResponse{}, HttpError{Op: "reading response body", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: err}
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorReadingResponseBody, err: err}
|
||||
}
|
||||
|
||||
var data WellKnownResponse
|
||||
var data SessionResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Error().Str("url", wellKnownUrl).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
return WellKnownResponse{}, HttpError{Op: "reading decoding response JSON payload", Method: http.MethodGet, Url: wellKnownUrl, Username: username, Err: err}
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, error) {
|
||||
func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
|
||||
jmapUrl := session.JmapUrl.String()
|
||||
|
||||
bodyBytes, marshalErr := json.Marshal(request)
|
||||
if marshalErr != nil {
|
||||
logger.Error().Err(marshalErr).Msg("failed to marshall JSON payload")
|
||||
return nil, marshalErr
|
||||
bodyBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to marshall JSON payload")
|
||||
return nil, SimpleError{code: JmapErrorEncodingRequestBody, err: err}
|
||||
}
|
||||
|
||||
req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
|
||||
if reqErr != nil {
|
||||
logger.Error().Err(reqErr).Msgf("failed to create GET request for %v", jmapUrl)
|
||||
return nil, reqErr
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl)
|
||||
return nil, SimpleError{code: JmapErrorCreatingRequest, err: err}
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("User-Agent", h.userAgent)
|
||||
@@ -144,12 +129,12 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", jmapUrl)
|
||||
return nil, HttpError{Op: "performing request", Method: http.MethodPost, Url: jmapUrl, Username: session.Username, Err: err}
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
|
||||
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
return nil, HttpError{Op: "processing response", Method: http.MethodPost, Url: jmapUrl, Username: session.Username, Err: fmt.Errorf("status is %v", res.Status)}
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
if res.Body != nil {
|
||||
defer func(Body io.ReadCloser) {
|
||||
@@ -163,7 +148,7 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
return nil, HttpError{Op: "reading response body", Method: http.MethodPost, Url: jmapUrl, Username: session.Username, Err: err}
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
|
||||
@@ -23,23 +23,23 @@ const (
|
||||
JmapKeywordMdnSent = "$mdnsent"
|
||||
)
|
||||
|
||||
type WellKnownAccount struct {
|
||||
type SessionAccount struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
IsPersonal bool `json:"isPersonal"`
|
||||
IsReadOnly bool `json:"isReadOnly"`
|
||||
AccountCapabilities map[string]any `json:"accountCapabilities,omitempty"`
|
||||
}
|
||||
|
||||
type WellKnownResponse struct {
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
Accounts map[string]WellKnownAccount `json:"accounts,omitempty"`
|
||||
PrimaryAccounts map[string]string `json:"primaryAccounts,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
ApiUrl string `json:"apiUrl,omitempty"`
|
||||
DownloadUrl string `json:"downloadUrl,omitempty"`
|
||||
UploadUrl string `json:"uploadUrl,omitempty"`
|
||||
EventSourceUrl string `json:"eventSourceUrl,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
type SessionResponse struct {
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
Accounts map[string]SessionAccount `json:"accounts,omitempty"`
|
||||
PrimaryAccounts map[string]string `json:"primaryAccounts,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
ApiUrl string `json:"apiUrl,omitempty"`
|
||||
DownloadUrl string `json:"downloadUrl,omitempty"`
|
||||
UploadUrl string `json:"uploadUrl,omitempty"`
|
||||
EventSourceUrl string `json:"eventSourceUrl,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
type Mailbox struct {
|
||||
|
||||
@@ -19,7 +19,7 @@ type TestJmapWellKnownClient struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func NewTestJmapWellKnownClient(t *testing.T) WellKnownClient {
|
||||
func NewTestJmapWellKnownClient(t *testing.T) SessionClient {
|
||||
return &TestJmapWellKnownClient{t: t}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ func (t *TestJmapWellKnownClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestJmapWellKnownClient) GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) {
|
||||
return WellKnownResponse{
|
||||
func (t *TestJmapWellKnownClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
return SessionResponse{
|
||||
Username: generateRandomString(8),
|
||||
ApiUrl: "test://",
|
||||
PrimaryAccounts: map[string]string{JmapMail: generateRandomString(2 + seededRand.Intn(10))},
|
||||
@@ -47,12 +47,12 @@ func (t TestJmapApiClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveTestFile(t *testing.T, name string) ([]byte, error) {
|
||||
func serveTestFile(t *testing.T, name string) ([]byte, Error) {
|
||||
cwd, _ := os.Getwd()
|
||||
p := filepath.Join(cwd, "testdata", name)
|
||||
bytes, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return bytes, err
|
||||
return bytes, SimpleError{code: 0, err: err}
|
||||
}
|
||||
// try to parse it first to avoid any deeper issues that are caused by the test tools
|
||||
var target map[string]any
|
||||
@@ -60,10 +60,10 @@ func serveTestFile(t *testing.T, name string) ([]byte, error) {
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse JSON test data file '%v': %v", p, err)
|
||||
}
|
||||
return bytes, err
|
||||
return bytes, SimpleError{code: 0, err: err}
|
||||
}
|
||||
|
||||
func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, error) {
|
||||
func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
|
||||
command := request.MethodCalls[0].Command
|
||||
switch command {
|
||||
case MailboxGet:
|
||||
@@ -72,7 +72,7 @@ func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
return serveTestFile(t.t, "mails1.json")
|
||||
default:
|
||||
require.Fail(t.t, "TestJmapApiClient: unsupported jmap command: %v", command)
|
||||
return nil, fmt.Errorf("TestJmapApiClient: unsupported jmap command: %v", command)
|
||||
return nil, SimpleError{code: 0, err: fmt.Errorf("TestJmapApiClient: unsupported jmap command: %v", command)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@ func command[T any](api ApiClient,
|
||||
ctx context.Context,
|
||||
session *Session,
|
||||
request Request,
|
||||
mapper func(body *Response) (T, error)) (T, error) {
|
||||
mapper func(body *Response) (T, Error)) (T, Error) {
|
||||
|
||||
responseBody, err := api.Command(ctx, logger, session, request)
|
||||
if err != nil {
|
||||
responseBody, jmapErr := api.Command(ctx, logger, session, request)
|
||||
if jmapErr != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
return zero, jmapErr
|
||||
}
|
||||
|
||||
var data Response
|
||||
err = json.Unmarshal(responseBody, &data)
|
||||
err := json.Unmarshal(responseBody, &data)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to deserialize body JSON payload")
|
||||
var zero T
|
||||
return zero, err
|
||||
return zero, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
return mapper(&data)
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
|
||||
func TestDeserializeMailboxGetResponse(t *testing.T) {
|
||||
require := require.New(t)
|
||||
jsonBytes, err := serveTestFile(t, "mailboxes1.json")
|
||||
require.NoError(err)
|
||||
jsonBytes, jmapErr := serveTestFile(t, "mailboxes1.json")
|
||||
require.NoError(jmapErr)
|
||||
var data Response
|
||||
err = json.Unmarshal(jsonBytes, &data)
|
||||
err := json.Unmarshal(jsonBytes, &data)
|
||||
require.NoError(err)
|
||||
require.Empty(data.CreatedIds)
|
||||
require.Equal("3e25b2a0", data.SessionState)
|
||||
@@ -62,10 +62,10 @@ func TestDeserializeMailboxGetResponse(t *testing.T) {
|
||||
|
||||
func TestDeserializeEmailGetResponse(t *testing.T) {
|
||||
require := require.New(t)
|
||||
jsonBytes, err := serveTestFile(t, "mails1.json")
|
||||
require.NoError(err)
|
||||
jsonBytes, jmapErr := serveTestFile(t, "mails1.json")
|
||||
require.NoError(jmapErr)
|
||||
var data Response
|
||||
err = json.Unmarshal(jsonBytes, &data)
|
||||
err := json.Unmarshal(jsonBytes, &data)
|
||||
require.NoError(err)
|
||||
require.Empty(data.CreatedIds)
|
||||
require.Equal("3e25b2a0", data.SessionState)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
func (g Groupware) Route(r chi.Router) {
|
||||
r.Get("/", g.Index)
|
||||
r.Get("/ping", g.Ping)
|
||||
r.Get("/mailboxes", g.GetMailboxes) // ?name=&role=&subcribed=
|
||||
r.Get("/mailbox/{id}", g.GetMailboxById)
|
||||
r.Get("/{mailbox}/messages", g.GetMessages)
|
||||
@@ -30,11 +29,6 @@ func (IndexResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Groupware) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
g.logger.Info().Msg("groupware pinged")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (g Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
@@ -55,16 +49,16 @@ func (g Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (g Groupware) GetIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
res, err := g.jmap.GetIdentity(session, ctx, logger)
|
||||
return res, res.State, err
|
||||
return res, res.State, apiErrorFromJmap(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (g Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
res, err := g.jmap.GetVacationResponse(session, ctx, logger)
|
||||
return res, res.State, err
|
||||
return res, res.State, apiErrorFromJmap(err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,16 +69,16 @@ func (g Groupware) GetMailboxById(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
res, err := g.jmap.GetMailbox(session, ctx, logger, []string{mailboxId})
|
||||
if err != nil {
|
||||
return res, "", err
|
||||
return res, "", apiErrorFromJmap(err)
|
||||
}
|
||||
|
||||
if len(res.List) == 1 {
|
||||
return res.List[0], res.State, err
|
||||
return res.List[0], res.State, apiErrorFromJmap(err)
|
||||
} else {
|
||||
return nil, res.State, err
|
||||
return nil, res.State, apiErrorFromJmap(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -116,18 +110,18 @@ func (g Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if hasCriteria {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
mailboxes, err := g.jmap.SearchMailboxes(session, ctx, logger, filter)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", apiErrorFromJmap(err)
|
||||
}
|
||||
return mailboxes.Mailboxes, mailboxes.State, nil
|
||||
})
|
||||
} else {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
mailboxes, err := g.jmap.GetAllMailboxes(session, ctx, logger)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", apiErrorFromJmap(err)
|
||||
}
|
||||
return mailboxes.List, mailboxes.State, nil
|
||||
})
|
||||
@@ -136,7 +130,7 @@ func (g Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, "mailbox")
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error) {
|
||||
g.respond(w, r, func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError) {
|
||||
page, ok, _ := ParseNumericParam(r, "page", -1)
|
||||
if ok {
|
||||
logger = &log.Logger{Logger: logger.With().Int("page", page).Logger()}
|
||||
@@ -154,7 +148,7 @@ func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
emails, err := g.jmap.GetEmails(session, ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", apiErrorFromJmap(err)
|
||||
}
|
||||
|
||||
return emails, emails.State, nil
|
||||
|
||||
295
services/groupware/pkg/groupware/groupware_api_error.go
Normal file
295
services/groupware/pkg/groupware/groupware_api_error.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Href string `json:"href"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorLinks struct {
|
||||
About any `json:"about,omitempty"`
|
||||
Type any `json:"type"` // either a string containing an URL, or a Link object
|
||||
}
|
||||
|
||||
type ErrorSource struct {
|
||||
Pointer string `json:"pointer,omitempty"` // a JSON Pointer [RFC6901] to the value in the request document that caused the error
|
||||
Parameter string `json:"parameter,omitempty"` // a string indicating which URI query parameter caused the error
|
||||
Header string `json:"header,omitempty"` // a string indicating the name of a single request header which caused the error
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
Id string `json:"id"` // a unique identifier for this particular occurrence of the problem
|
||||
Links *ErrorLinks `json:"links,omitempty"`
|
||||
NumStatus int `json:"-"`
|
||||
Status string `json:"status"` // the HTTP status code applicable to this problem, expressed as a string value
|
||||
Code string `json:"code"` // an application-specific error code, expressed as a string value
|
||||
Title string `json:"title,omitempty"` // a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem
|
||||
Detail string `json:"detail,omitempty"` // a human-readable explanation specific to this occurrence of the problem
|
||||
Source *ErrorSource `json:"source,omitempty"` // an object containing references to the primary source of the error
|
||||
Meta map[string]any `json:"meta,omitempty"` // a meta object containing non-standard meta-information about the error
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Errors []ApiError `json:"errors"`
|
||||
}
|
||||
|
||||
var _ render.Renderer = ErrorResponse{}
|
||||
|
||||
func (e ErrorResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Add("Content-Type", ContentTypeJsonApi)
|
||||
if len(e.Errors) > 0 {
|
||||
render.Status(r, e.Errors[0].NumStatus)
|
||||
} else {
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ContentTypeJsonApi = "application/vnd.api+json"
|
||||
)
|
||||
|
||||
type GroupwareError struct {
|
||||
Status int
|
||||
Code string
|
||||
Title string
|
||||
Detail string
|
||||
}
|
||||
|
||||
func groupwareErrorFromJmap(j jmap.Error) *GroupwareError {
|
||||
if j == nil {
|
||||
return nil
|
||||
}
|
||||
switch j.Code() {
|
||||
case jmap.JmapErrorAuthenticationFailed:
|
||||
return &ErrorForbidden
|
||||
case jmap.JmapErrorInvalidHttpRequest:
|
||||
return &ErrorInvalidRequest
|
||||
case jmap.JmapErrorServerResponse:
|
||||
return &ErrorServerResponse
|
||||
case jmap.JmapErrorReadingResponseBody:
|
||||
return &ErrorReadingResponse
|
||||
case jmap.JmapErrorDecodingResponseBody:
|
||||
return &ErrorProcessingResponse
|
||||
case jmap.JmapErrorEncodingRequestBody:
|
||||
return &ErrorEncodingRequestBody
|
||||
case jmap.JmapErrorCreatingRequest:
|
||||
return &ErrorCreatingRequest
|
||||
case jmap.JmapErrorSendingRequest:
|
||||
return &ErrorSendingRequest
|
||||
case jmap.JmapErrorInvalidSessionResponse:
|
||||
return &ErrorInvalidSessionResponse
|
||||
case jmap.JmapErrorInvalidJmapRequestPayload:
|
||||
return &ErrorInvalidRequestPayload
|
||||
case jmap.JmapErrorInvalidJmapResponsePayload:
|
||||
return &ErrorInvalidResponsePayload
|
||||
default:
|
||||
return &ErrorGeneric
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ErrorCodeGeneric = "ERRGEN"
|
||||
ErrorCodeMissingAuthentication = "AUTMIS"
|
||||
ErrorCodeForbiddenGeneric = "AUTFOR"
|
||||
ErrorCodeInvalidRequest = "INVREQ"
|
||||
ErrorCodeServerResponse = "SRVRSP"
|
||||
ErrorCodeServerReadingResponse = "SRVRRE"
|
||||
ErrorCodeServerDecodingResponseBody = "SRVDRB"
|
||||
ErrorCodeEncodingRequestBody = "ENCREQ"
|
||||
ErrorCodeCreatingRequest = "CREREQ"
|
||||
ErrorCodeSendingRequest = "SNDREQ"
|
||||
ErrorCodeInvalidSessionResponse = "INVSES"
|
||||
ErrorCodeInvalidRequestPayload = "INVRQP"
|
||||
ErrorCodeInvalidResponsePayload = "INVRSP"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorGeneric = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeGeneric,
|
||||
Title: "Unspecific Error",
|
||||
Detail: "Error without a specific description.",
|
||||
}
|
||||
ErrorMissingAuthentication = GroupwareError{
|
||||
Status: http.StatusUnauthorized,
|
||||
Code: ErrorCodeMissingAuthentication,
|
||||
Title: "Missing Authentication",
|
||||
Detail: "No authentication credentials were provided.",
|
||||
}
|
||||
ErrorForbidden = GroupwareError{
|
||||
Status: http.StatusForbidden,
|
||||
Code: ErrorCodeForbiddenGeneric,
|
||||
Title: "Invalid Authentication",
|
||||
Detail: "Authentication credentials were provided but are either invalid or not authorized to perform the request operation.",
|
||||
}
|
||||
ErrorInvalidRequest = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeInvalidRequest,
|
||||
Title: "Invalid Request",
|
||||
Detail: "The request that was meant to be sent to the mail server is invalid, which might be caused by configuration issues.",
|
||||
}
|
||||
ErrorServerResponse = GroupwareError{
|
||||
Status: http.StatusServiceUnavailable,
|
||||
Code: ErrorCodeServerResponse,
|
||||
Title: "Server responds with an Error",
|
||||
Detail: "The mail server responded with an error.",
|
||||
}
|
||||
ErrorReadingResponse = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeServerResponse,
|
||||
Title: "Server Response Body could not be decoded",
|
||||
Detail: "The mail server response body could not be decoded.",
|
||||
}
|
||||
ErrorProcessingResponse = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeServerResponse,
|
||||
Title: "Server Response Body could not be decoded",
|
||||
Detail: "The mail server response body could not be decoded.",
|
||||
}
|
||||
ErrorEncodingRequestBody = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeEncodingRequestBody,
|
||||
Title: "Failed to encode the Request Body",
|
||||
Detail: "Failed to encode the body of the request to be sent to the mail server.",
|
||||
}
|
||||
ErrorCreatingRequest = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeCreatingRequest,
|
||||
Title: "Failed to create the Request",
|
||||
Detail: "Failed to create the request to be sent to the mail server.",
|
||||
}
|
||||
ErrorSendingRequest = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeSendingRequest,
|
||||
Title: "Failed to send the Request",
|
||||
Detail: "Failed to send the request to the mail server.",
|
||||
}
|
||||
ErrorInvalidSessionResponse = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeInvalidSessionResponse,
|
||||
Title: "Invalid JMAP Session Response",
|
||||
Detail: "The JMAP session response that was provided by the mail server is invalid.",
|
||||
}
|
||||
ErrorInvalidRequestPayload = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeInvalidRequestPayload,
|
||||
Title: "Invalid Request Payload",
|
||||
Detail: "The request to the mail server is invalid.",
|
||||
}
|
||||
ErrorInvalidResponsePayload = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeInvalidResponsePayload,
|
||||
Title: "Invalid Response Payload",
|
||||
Detail: "The payload of the response received from the mail server is invalid.",
|
||||
}
|
||||
)
|
||||
|
||||
type ErrorOpt interface {
|
||||
apply(error *ApiError)
|
||||
}
|
||||
|
||||
type ErrorLinksOpt struct {
|
||||
links *ErrorLinks
|
||||
}
|
||||
|
||||
func (o ErrorLinksOpt) apply(error *ApiError) {
|
||||
error.Links = o.links
|
||||
}
|
||||
|
||||
type SourceLinksOpt struct {
|
||||
source *ErrorSource
|
||||
}
|
||||
|
||||
func (o SourceLinksOpt) apply(error *ApiError) {
|
||||
error.Source = o.source
|
||||
}
|
||||
|
||||
type MetaLinksOpt struct {
|
||||
meta map[string]any
|
||||
}
|
||||
|
||||
func (o MetaLinksOpt) apply(error *ApiError) {
|
||||
error.Meta = o.meta
|
||||
}
|
||||
|
||||
type CodeOpt struct {
|
||||
code string
|
||||
}
|
||||
|
||||
func (o CodeOpt) apply(error *ApiError) {
|
||||
error.Code = o.code
|
||||
}
|
||||
|
||||
type TitleOpt struct {
|
||||
title string
|
||||
detail string
|
||||
}
|
||||
|
||||
func (o TitleOpt) apply(error *ApiError) {
|
||||
error.Title = o.title
|
||||
error.Detail = o.detail
|
||||
}
|
||||
|
||||
func errorResponse(id string, error GroupwareError, options ...ErrorOpt) ErrorResponse {
|
||||
err := ApiError{
|
||||
Id: id,
|
||||
NumStatus: error.Status,
|
||||
Status: strconv.Itoa(error.Status),
|
||||
Code: error.Code,
|
||||
Title: error.Title,
|
||||
Detail: error.Detail,
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o.apply(&err)
|
||||
}
|
||||
|
||||
return ErrorResponse{
|
||||
Errors: []ApiError{err},
|
||||
}
|
||||
}
|
||||
|
||||
func apiError(id string, error GroupwareError, options ...ErrorOpt) ApiError {
|
||||
err := ApiError{
|
||||
Id: id,
|
||||
NumStatus: error.Status,
|
||||
Status: strconv.Itoa(error.Status),
|
||||
Code: error.Code,
|
||||
Title: error.Title,
|
||||
Detail: error.Detail,
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o.apply(&err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func apiErrorFromJmap(error jmap.Error) *ApiError {
|
||||
if error == nil {
|
||||
return nil
|
||||
}
|
||||
gwe := groupwareErrorFromJmap(error)
|
||||
if gwe == nil {
|
||||
return nil
|
||||
}
|
||||
api := apiError(uuid.NewString(), *gwe)
|
||||
return &api
|
||||
}
|
||||
|
||||
func errorResponses(errors ...ApiError) ErrorResponse {
|
||||
return ErrorResponse{Errors: errors}
|
||||
}
|
||||
@@ -124,27 +124,11 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) (*Gro
|
||||
return nil, GroupwareInitializationError{Message: "Mail.Master.Password is empty"}
|
||||
}
|
||||
|
||||
defaultEmailLimit := config.Mail.DefaultEmailLimit
|
||||
if defaultEmailLimit < 0 {
|
||||
defaultEmailLimit = 0
|
||||
}
|
||||
maxBodyValueBytes := config.Mail.MaxBodyValueBytes
|
||||
if maxBodyValueBytes < 0 {
|
||||
maxBodyValueBytes = 0
|
||||
}
|
||||
|
||||
responseHeaderTimeout := config.Mail.ResponseHeaderTimeout
|
||||
if responseHeaderTimeout < 0 {
|
||||
responseHeaderTimeout = 0
|
||||
}
|
||||
sessionCacheTtl := config.Mail.SessionCacheTtl
|
||||
if sessionCacheTtl < 0 {
|
||||
sessionCacheTtl = 0
|
||||
}
|
||||
sessionFailureCacheTtl := config.Mail.SessionFailureCacheTtl
|
||||
if sessionFailureCacheTtl < 0 {
|
||||
sessionFailureCacheTtl = 0
|
||||
}
|
||||
defaultEmailLimit := max(config.Mail.DefaultEmailLimit, 0)
|
||||
maxBodyValueBytes := max(config.Mail.MaxBodyValueBytes, 0)
|
||||
responseHeaderTimeout := max(config.Mail.ResponseHeaderTimeout, 0)
|
||||
sessionCacheTtl := max(config.Mail.SessionCacheTtl, 0)
|
||||
sessionFailureCacheTtl := max(config.Mail.SessionFailureCacheTtl, 0)
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = responseHeaderTimeout
|
||||
@@ -223,7 +207,7 @@ func (g Groupware) session(req *http.Request, ctx context.Context, logger *log.L
|
||||
}
|
||||
|
||||
func (g Groupware) respond(w http.ResponseWriter, r *http.Request,
|
||||
handler func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, error)) {
|
||||
handler func(r *http.Request, ctx context.Context, logger *log.Logger, session *jmap.Session) (any, string, *ApiError)) {
|
||||
ctx := r.Context()
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
session, ok, err := g.session(r, ctx, &logger)
|
||||
@@ -240,10 +224,10 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
logger = session.DecorateLogger(logger)
|
||||
|
||||
response, state, err := handler(r, ctx, &logger, &session)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg(err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
response, state, apierr := handler(r, ctx, &logger, &session)
|
||||
if apierr != nil {
|
||||
logger.Warn().Interface("error", apierr).Msgf("API error: %v", apierr)
|
||||
render.Render(w, r, errorResponses(*apierr))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -251,7 +235,7 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request,
|
||||
w.Header().Add("ETag", state)
|
||||
}
|
||||
if response == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
render.Status(r, http.StatusNotFound)
|
||||
} else {
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, response)
|
||||
|
||||
Reference in New Issue
Block a user