mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-13 15:49:38 -06:00
Groupware improvements
* ensure that all the jmap responses contain the SessionState * implement missing errors that were marked as TODO * moved common functions from pkg/jmap and pkg/services/groupware to pkg/log and pkg/structs to commonalize them across both source trees * implement error handling for SetError occurences * Email: replace anonymous map[string]bool for mailbox rights with a MailboxRights struct, as the keys are well-defined, which allows for properly documenting them * introduce ObjectType as an "enum" * fix JSON marshalling and unmarshalling of EmailBodyStructure * move the swagger documentation structs from groupware_api.go to groupware_docs.go * fix: change verb for /groupware/accounts/*/vacation from POST to PUT
This commit is contained in:
@@ -9,7 +9,13 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, id string) (*Blob, Error) {
|
||||
type BlobResponse struct {
|
||||
Blob *Blob `json:"blob,omitempty"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, id string) (BlobResponse, Error) {
|
||||
aid := session.BlobAccountId(accountId)
|
||||
|
||||
cmd, err := request(
|
||||
@@ -20,29 +26,31 @@ func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context
|
||||
}, "0"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
return BlobResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (*Blob, Error) {
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (BlobResponse, Error) {
|
||||
var response BlobGetResponse
|
||||
err = retrieveResponseMatchParameters(body, BlobGet, "0", &response)
|
||||
if err != nil {
|
||||
return nil, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
return BlobResponse{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
|
||||
if len(response.List) != 1 {
|
||||
return nil, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
return BlobResponse{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
get := response.List[0]
|
||||
return &get, nil
|
||||
return BlobResponse{Blob: &get, State: response.State, SessionState: body.SessionState}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type UploadedBlob struct {
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) UploadBlobStream(accountId string, session *Session, ctx context.Context, logger *log.Logger, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
@@ -60,7 +68,7 @@ func (j *Client) DownloadBlobStream(accountId string, blobId string, name string
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{blobId}", blobId)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{name}", name)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{type}", typ)
|
||||
logger = &log.Logger{Logger: logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId).Str(logAccountId, aid).Logger()}
|
||||
logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId).Str(logAccountId, aid))
|
||||
return j.blob.DownloadBinary(ctx, logger, session, downloadUrl)
|
||||
}
|
||||
|
||||
@@ -126,10 +134,12 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
|
||||
get := getResponse.List[0]
|
||||
|
||||
return UploadedBlob{
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
State: getResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package jmap
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -22,11 +23,12 @@ const (
|
||||
)
|
||||
|
||||
type Emails struct {
|
||||
Emails []Email `json:"emails,omitempty"`
|
||||
Total int `json:"total,omitzero"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Offset int `json:"offset,omitzero"`
|
||||
State string `json:"state,omitempty"`
|
||||
Emails []Email `json:"emails,omitempty"`
|
||||
Total int `json:"total,omitzero"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Offset int `json:"offset,omitzero"`
|
||||
State string `json:"state,omitempty"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes int) (Emails, Error) {
|
||||
@@ -48,7 +50,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
|
||||
if err != nil {
|
||||
return Emails{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
return Emails{Emails: response.List, State: body.SessionState}, nil
|
||||
return Emails{Emails: response.List, State: response.State, SessionState: body.SessionState}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,11 +104,12 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
}
|
||||
|
||||
return Emails{
|
||||
Emails: getResponse.List,
|
||||
State: body.SessionState,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Offset: queryResponse.Position,
|
||||
Emails: getResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Offset: queryResponse.Position,
|
||||
SessionState: body.SessionState,
|
||||
State: getResponse.State,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -118,6 +121,7 @@ type EmailsSince struct {
|
||||
Created []Email `json:"created,omitempty"`
|
||||
Updated []Email `json:"updated,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, since string, fetchBodies bool, maxBodyValueBytes int, maxChanges int) (EmailsSince, Error) {
|
||||
@@ -185,7 +189,8 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
NewState: mailboxResponse.NewState,
|
||||
Created: createdResponse.List,
|
||||
Updated: createdResponse.List,
|
||||
State: body.SessionState,
|
||||
State: createdResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -255,7 +260,8 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
NewState: changesResponse.NewState,
|
||||
Created: createdResponse.List,
|
||||
Updated: createdResponse.List,
|
||||
State: body.SessionState,
|
||||
State: updatedResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -323,10 +329,10 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement,
|
||||
|
||||
return EmailSnippetQueryResult{
|
||||
Snippets: snippetResponse.List,
|
||||
QueryState: queryResponse.QueryState,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
@@ -340,10 +346,10 @@ type EmailWithSnippets struct {
|
||||
|
||||
type EmailQueryResult struct {
|
||||
Results []EmailWithSnippets `json:"results"`
|
||||
QueryState string `json:"queryState"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Position int `json:"position,omitzero"`
|
||||
QueryState string `json:"queryState"`
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
}
|
||||
|
||||
@@ -440,10 +446,10 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
|
||||
return EmailQueryResult{
|
||||
Results: results,
|
||||
QueryState: queryResponse.QueryState,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
@@ -451,10 +457,11 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
}
|
||||
|
||||
type UploadedEmail struct {
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (UploadedEmail, Error) {
|
||||
@@ -519,18 +526,20 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
|
||||
get := getResponse.List[0]
|
||||
|
||||
return UploadedEmail{
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type CreatedEmail struct {
|
||||
Email Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
Email Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger) (CreatedEmail, Error) {
|
||||
@@ -560,22 +569,29 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Sessi
|
||||
// TODO(pbleser-oc) handle submission errors
|
||||
}
|
||||
|
||||
setErr, notok := setResponse.NotCreated["c"]
|
||||
if notok {
|
||||
return CreatedEmail{}, setErrorError(setErr, EmailType)
|
||||
}
|
||||
|
||||
created, ok := setResponse.Created["c"]
|
||||
if !ok {
|
||||
// failed to create?
|
||||
// TODO(pbleser-oc) handle email creation failure
|
||||
err = fmt.Errorf("failed to find %s in %s response", string(EmailType), string(EmailSet))
|
||||
return CreatedEmail{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
return CreatedEmail{
|
||||
Email: created,
|
||||
State: setResponse.NewState,
|
||||
Email: created,
|
||||
State: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type UpdatedEmails struct {
|
||||
Updated map[string]Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
Updated map[string]Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
// The Email/set method encompasses:
|
||||
@@ -610,14 +626,16 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
|
||||
// TODO(pbleser-oc) handle submission errors
|
||||
}
|
||||
return UpdatedEmails{
|
||||
Updated: setResponse.Updated,
|
||||
State: setResponse.NewState,
|
||||
Updated: setResponse.Updated,
|
||||
State: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type DeletedEmails struct {
|
||||
State string `json:"state"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger) (DeletedEmails, Error) {
|
||||
@@ -643,7 +661,7 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi
|
||||
// error occured
|
||||
// TODO(pbleser-oc) handle submission errors
|
||||
}
|
||||
return DeletedEmails{State: setResponse.NewState}, nil
|
||||
return DeletedEmails{State: setResponse.NewState, SessionState: body.SessionState}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -670,6 +688,8 @@ type SubmittedEmail struct {
|
||||
//
|
||||
// [RFC8098]: https://datatracker.ietf.org/doc/html/rfc8098
|
||||
MdnBlobIds []string `json:"mdnBlobIds,omitempty"`
|
||||
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (SubmittedEmail, Error) {
|
||||
@@ -744,14 +764,15 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
submission := getResponse.List[0]
|
||||
|
||||
return SubmittedEmail{
|
||||
Id: submission.Id,
|
||||
State: setResponse.NewState,
|
||||
SendAt: submission.SendAt,
|
||||
ThreadId: submission.ThreadId,
|
||||
UndoStatus: submission.UndoStatus,
|
||||
Envelope: *submission.Envelope,
|
||||
DsnBlobIds: submission.DsnBlobIds,
|
||||
MdnBlobIds: submission.MdnBlobIds,
|
||||
Id: submission.Id,
|
||||
State: setResponse.NewState,
|
||||
SendAt: submission.SendAt,
|
||||
ThreadId: submission.ThreadId,
|
||||
UndoStatus: submission.UndoStatus,
|
||||
Envelope: *submission.Envelope,
|
||||
DsnBlobIds: submission.DsnBlobIds,
|
||||
MdnBlobIds: submission.MdnBlobIds,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
|
||||
@@ -5,35 +5,47 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Identities struct {
|
||||
Identities []Identity `json:"identities"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#identityget
|
||||
func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (IdentityGetResponse, Error) {
|
||||
func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (Identities, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetIdentity", session, logger)
|
||||
cmd, err := request(invocation(IdentityGet, IdentityGetCommand{AccountId: aid}, "0"))
|
||||
if err != nil {
|
||||
return IdentityGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
return Identities{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentityGetResponse, Error) {
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Identities, Error) {
|
||||
var response IdentityGetResponse
|
||||
err = retrieveResponseMatchParameters(body, IdentityGet, "0", &response)
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
return Identities{
|
||||
Identities: response.List,
|
||||
State: response.State,
|
||||
SessionState: body.SessionState,
|
||||
}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
type IdentitiesGetResponse struct {
|
||||
State string `json:"state"`
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, Error) {
|
||||
uniqueAccountIds := uniq(accountIds)
|
||||
uniqueAccountIds := structs.Uniq(accountIds)
|
||||
|
||||
logger = j.loggerParams("", "GetIdentities", session, logger, func(l zerolog.Context) zerolog.Context {
|
||||
return l.Array(logAccountId, logstrarray(uniqueAccountIds))
|
||||
return l.Array(logAccountId, log.SafeStringArray(uniqueAccountIds))
|
||||
})
|
||||
|
||||
calls := make([]Invocation, len(uniqueAccountIds))
|
||||
@@ -62,9 +74,10 @@ func (j *Client) GetIdentities(accountIds []string, session *Session, ctx contex
|
||||
}
|
||||
|
||||
return IdentitiesGetResponse{
|
||||
Identities: identities,
|
||||
State: lastState,
|
||||
NotFound: uniq(notFound),
|
||||
Identities: identities,
|
||||
NotFound: structs.Uniq(notFound),
|
||||
State: lastState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,43 +6,63 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
type MailboxesResponse struct {
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
NotFound []any `json:"notFound"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxget
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxGetResponse, Error) {
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetMailbox", session, logger)
|
||||
cmd, err := request(invocation(MailboxGet, MailboxGetCommand{AccountId: aid, Ids: ids}, "0"))
|
||||
if err != nil {
|
||||
return MailboxGetResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
return MailboxesResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxGetResponse, Error) {
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxesResponse, Error) {
|
||||
var response MailboxGetResponse
|
||||
err = retrieveResponseMatchParameters(body, MailboxGet, "0", &response)
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
if err != nil {
|
||||
return MailboxesResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
return MailboxesResponse{
|
||||
Mailboxes: response.List,
|
||||
NotFound: response.NotFound,
|
||||
State: response.State,
|
||||
SessionState: body.SessionState,
|
||||
}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) GetAllMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger) (MailboxGetResponse, Error) {
|
||||
return j.GetMailbox(accountId, session, ctx, logger, nil)
|
||||
type AllMailboxesResponse struct {
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxquery
|
||||
func (j *Client) QueryMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (MailboxQueryResponse, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "QueryMailbox", session, logger)
|
||||
cmd, err := request(invocation(MailboxQuery, MailboxQueryCommand{AccountId: aid, Filter: filter}, "0"))
|
||||
func (j *Client) GetAllMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger) (AllMailboxesResponse, Error) {
|
||||
resp, err := j.GetMailbox(accountId, session, ctx, logger, nil)
|
||||
if err != nil {
|
||||
return MailboxQueryResponse{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
return AllMailboxesResponse{}, err
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxQueryResponse, Error) {
|
||||
var response MailboxQueryResponse
|
||||
err = retrieveResponseMatchParameters(body, MailboxQuery, "0", &response)
|
||||
return response, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
return AllMailboxesResponse{
|
||||
Mailboxes: resp.Mailboxes,
|
||||
State: resp.State,
|
||||
SessionState: resp.SessionState,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Mailboxes struct {
|
||||
// The list of mailboxes that were found using the specified search criteria.
|
||||
Mailboxes []Mailbox `json:"mailboxes,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
// The state of the search.
|
||||
State string `json:"state,omitempty"`
|
||||
// The state of the Session.
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (Mailboxes, Error) {
|
||||
@@ -66,6 +86,6 @@ func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context
|
||||
if err != nil {
|
||||
return Mailboxes{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err}
|
||||
}
|
||||
return Mailboxes{Mailboxes: response.List, State: body.SessionState}, nil
|
||||
return Mailboxes{Mailboxes: response.List, State: response.State, SessionState: body.SessionState}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package jmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -26,48 +27,8 @@ func (j *Client) GetVacationResponse(accountId string, session *Session, ctx con
|
||||
})
|
||||
}
|
||||
|
||||
type VacationResponseStatusChange struct {
|
||||
VacationResponse VacationResponse `json:"vacationResponse"`
|
||||
ResponseState string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) SetVacationResponseStatus(accountId string, enabled bool, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseStatusChange, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "EnableVacationResponse", session, logger)
|
||||
|
||||
cmd, err := request(invocation(VacationResponseSet, VacationResponseSetRequest{
|
||||
AccountId: aid,
|
||||
Update: map[string]PatchObject{
|
||||
"u": {
|
||||
"/isEnabled": enabled,
|
||||
},
|
||||
},
|
||||
}, "0"))
|
||||
|
||||
if err != nil {
|
||||
return VacationResponseStatusChange{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseStatusChange, Error) {
|
||||
var response VacationResponseSetResponse
|
||||
err = retrieveResponseMatchParameters(body, VacationResponseSet, "0", &response)
|
||||
if err != nil {
|
||||
return VacationResponseStatusChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
updated, ok := response.Updated["u"]
|
||||
if !ok {
|
||||
// TODO implement error when not updated
|
||||
}
|
||||
|
||||
return VacationResponseStatusChange{
|
||||
VacationResponse: updated,
|
||||
ResponseState: response.NewState,
|
||||
SessionState: response.State,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type VacationResponseBody struct {
|
||||
// Same as VacationResponse but without the id.
|
||||
type VacationResponsePayload struct {
|
||||
// Should a vacation response be sent if a message arrives between the "fromDate" and "toDate"?
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
// If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the
|
||||
@@ -96,44 +57,59 @@ type VacationResponseChange struct {
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponseBody, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, Error) {
|
||||
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "SetVacationResponse", session, logger)
|
||||
|
||||
set := VacationResponseSetRequest{
|
||||
AccountId: aid,
|
||||
Create: map[string]VacationResponse{
|
||||
vacationResponseId: {
|
||||
IsEnabled: vacation.IsEnabled,
|
||||
FromDate: vacation.FromDate,
|
||||
ToDate: vacation.ToDate,
|
||||
Subject: vacation.Subject,
|
||||
TextBody: vacation.TextBody,
|
||||
HtmlBody: vacation.HtmlBody,
|
||||
cmd, err := request(
|
||||
invocation(VacationResponseSet, VacationResponseSetCommand{
|
||||
AccountId: aid,
|
||||
Create: map[string]VacationResponse{
|
||||
vacationResponseId: {
|
||||
IsEnabled: vacation.IsEnabled,
|
||||
FromDate: vacation.FromDate,
|
||||
ToDate: vacation.ToDate,
|
||||
Subject: vacation.Subject,
|
||||
TextBody: vacation.TextBody,
|
||||
HtmlBody: vacation.HtmlBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmd, err := request(invocation(VacationResponseSet, set, "0"))
|
||||
}, "0"),
|
||||
// chain a second request to get the current complete VacationResponse object
|
||||
// after performing the changes, as that makes for a better API
|
||||
invocation(VacationResponseGet, VacationResponseGetCommand{AccountId: aid}, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
return VacationResponseChange{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err}
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseChange, Error) {
|
||||
var response VacationResponseSetResponse
|
||||
err = retrieveResponseMatchParameters(body, VacationResponseSet, "0", &response)
|
||||
var setResponse VacationResponseSetResponse
|
||||
err = retrieveResponseMatchParameters(body, VacationResponseSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
return VacationResponseChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
created, ok := response.Created[vacationResponseId]
|
||||
if !ok {
|
||||
// TODO handle case where created is missing
|
||||
setErr, notok := setResponse.NotCreated[vacationResponseId]
|
||||
if notok {
|
||||
// this means that the VacationResponse was not updated
|
||||
return VacationResponseChange{}, setErrorError(setErr, VacationResponseType)
|
||||
}
|
||||
|
||||
var getResponse VacationResponseGetResponse
|
||||
err = retrieveResponseMatchParameters(body, VacationResponseGet, "1", &getResponse)
|
||||
if err != nil {
|
||||
return VacationResponseChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
if len(getResponse.List) != 1 {
|
||||
err = fmt.Errorf("failed to find %s in %s response", string(VacationResponseType), string(VacationResponseGet))
|
||||
return VacationResponseChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
return VacationResponseChange{
|
||||
VacationResponse: created,
|
||||
ResponseState: response.NewState,
|
||||
SessionState: response.State,
|
||||
VacationResponse: getResponse.List[0],
|
||||
ResponseState: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,17 +48,17 @@ func (j *Client) FetchSession(username string, logger *log.Logger) (Session, Err
|
||||
}
|
||||
|
||||
func (j *Client) logger(accountId string, operation string, session *Session, logger *log.Logger) *log.Logger {
|
||||
zc := logger.With().Str(logOperation, operation).Str(logUsername, session.Username)
|
||||
l := logger.With().Str(logOperation, operation).Str(logUsername, session.Username)
|
||||
if accountId != "" {
|
||||
zc = zc.Str(logAccountId, accountId)
|
||||
l = l.Str(logAccountId, accountId)
|
||||
}
|
||||
return &log.Logger{Logger: zc.Logger()}
|
||||
return log.From(l)
|
||||
}
|
||||
|
||||
func (j *Client) loggerParams(accountId string, operation string, session *Session, logger *log.Logger, params func(zerolog.Context) zerolog.Context) *log.Logger {
|
||||
zc := logger.With().Str(logOperation, operation).Str(logUsername, session.Username)
|
||||
l := logger.With().Str(logOperation, operation).Str(logUsername, session.Username)
|
||||
if accountId != "" {
|
||||
zc = zc.Str(logAccountId, accountId)
|
||||
l = l.Str(logAccountId, accountId)
|
||||
}
|
||||
return &log.Logger{Logger: params(zc).Logger()}
|
||||
return log.From(l)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
JmapErrorAuthenticationFailed = iota
|
||||
JmapErrorInvalidHttpRequest
|
||||
@@ -13,6 +18,7 @@ const (
|
||||
JmapErrorInvalidJmapRequestPayload
|
||||
JmapErrorInvalidJmapResponsePayload
|
||||
JmapErrorMethodLevel
|
||||
JmapErrorSetError
|
||||
)
|
||||
|
||||
type Error interface {
|
||||
@@ -44,3 +50,13 @@ func simpleError(err error, code int) Error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setErrorError(err SetError, objectType ObjectType) Error {
|
||||
var e error
|
||||
if len(err.Properties) > 0 {
|
||||
e = fmt.Errorf("failed to modify %s due to %s error in properties [%s]: %s", objectType, err.Type, strings.Join(err.Properties, ", "), err.Description)
|
||||
} else {
|
||||
e = fmt.Errorf("failed to modify %s due to %s error: %s", objectType, err.Type, err.Description)
|
||||
}
|
||||
return SimpleError{code: JmapErrorSetError, err: e}
|
||||
}
|
||||
|
||||
@@ -303,18 +303,152 @@ const (
|
||||
Not FilterOperatorTerm = "NOT"
|
||||
)
|
||||
|
||||
type MailboxRights struct {
|
||||
// If true, the user may use this Mailbox as part of a filter in an Email/query call,
|
||||
// and the Mailbox may be included in the mailboxIds property of Email objects.
|
||||
//
|
||||
// Email objects may be fetched if they are in at least one Mailbox with this permission.
|
||||
//
|
||||
// If a sub-Mailbox is shared but not the parent Mailbox, this may be false.
|
||||
//
|
||||
// Corresponds to IMAP ACLs lr (if mapping from IMAP, both are required for this to be true).
|
||||
MayReadItems bool `json:"mayReadItems"`
|
||||
|
||||
// The user may add mail to this Mailbox (by either creating a new Email or moving an existing one).
|
||||
//
|
||||
// Corresponds to IMAP ACL i.
|
||||
MayAddItems bool `json:"mayAddItems"`
|
||||
|
||||
// The user may remove mail from this Mailbox (by either changing the Mailboxes of an Email or
|
||||
// destroying the Email).
|
||||
//
|
||||
// Corresponds to IMAP ACLs te (if mapping from IMAP, both are required for this to be true).
|
||||
MayRemoveItems bool `json:"mayRemoveItems"`
|
||||
|
||||
// The user may add or remove the $seen keyword to/from an Email.
|
||||
//
|
||||
// If an Email belongs to multiple Mailboxes, the user may only modify $seen if they have this
|
||||
// permission for all of the Mailboxes.
|
||||
//
|
||||
// Corresponds to IMAP ACL s.
|
||||
MaySetSeen bool `json:"maySetSeen"`
|
||||
|
||||
// The user may add or remove any keyword other than $seen to/from an Email.
|
||||
//
|
||||
// If an Email belongs to multiple Mailboxes, the user may only modify keywords if they have this
|
||||
// permission for all of the Mailboxes.
|
||||
//
|
||||
// Corresponds to IMAP ACL w.
|
||||
MaySetKeywords bool `json:"maySetKeywords"`
|
||||
|
||||
// The user may create a Mailbox with this Mailbox as its parent.
|
||||
//
|
||||
// Corresponds to IMAP ACL k.
|
||||
MayCreateChild bool `json:"mayCreateChild"`
|
||||
|
||||
// The user may rename the Mailbox or make it a child of another Mailbox.
|
||||
//
|
||||
// Corresponds to IMAP ACL x (although this covers both rename and delete permissions).
|
||||
MayRename bool `json:"mayRename"`
|
||||
|
||||
// The user may delete the Mailbox itself.
|
||||
//
|
||||
// Corresponds to IMAP ACL x (although this covers both rename and delete permissions).
|
||||
MayDelete bool `json:"mayDelete"`
|
||||
|
||||
// Messages may be submitted directly to this Mailbox.
|
||||
//
|
||||
// Corresponds to IMAP ACL p.
|
||||
MaySubmit bool `json:"maySubmit"`
|
||||
}
|
||||
|
||||
type Mailbox struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ParentId string `json:"parentId,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
IsSubscribed bool `json:"isSubscribed"`
|
||||
TotalEmails int `json:"totalEmails"`
|
||||
UnreadEmails int `json:"unreadEmails"`
|
||||
TotalThreads int `json:"totalThreads"`
|
||||
UnreadThreads int `json:"unreadThreads"`
|
||||
MyRights map[string]bool `json:"myRights,omitempty"`
|
||||
// The id of the Mailbox.
|
||||
Id string `json:"id,omitempty"`
|
||||
|
||||
// User-visible name for the Mailbox, e.g., “Inbox”.
|
||||
//
|
||||
// This MUST be a Net-Unicode string [@!RFC5198] of at least 1 character in length, subject to the maximum size
|
||||
// given in the capability object.
|
||||
//
|
||||
// There MUST NOT be two sibling Mailboxes with both the same parent and the same name.
|
||||
//
|
||||
// Servers MAY reject names that violate server policy (e.g., names containing a slash (/) or control characters).
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// The Mailbox id for the parent of this Mailbox, or null if this Mailbox is at the top level.
|
||||
//
|
||||
// Mailboxes form acyclic graphs (forests) directed by the child-to-parent relationship. There MUST NOT be a loop.
|
||||
ParentId string `json:"parentId,omitempty"`
|
||||
|
||||
// Identifies Mailboxes that have a particular common purpose (e.g., the “inbox”), regardless of the name property
|
||||
// (which may be localised).
|
||||
//
|
||||
// This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE extension [RFC6154]).
|
||||
// However, unlike in IMAP, a Mailbox MUST only have a single role, and there MUST NOT be two Mailboxes in the same
|
||||
// account with the same role.
|
||||
//
|
||||
// Servers providing IMAP access to the same data are encouraged to enforce these extra restrictions in IMAP as well.
|
||||
// Otherwise, modifying the IMAP attributes to ensure compliance when exposing the data over JMAP is implementation dependent.
|
||||
//
|
||||
// The value MUST be one of the Mailbox attribute names listed in the IANA IMAP Mailbox Name Attributes registry,
|
||||
// as established in [RFC8457], converted to lowercase. New roles may be established here in the future.
|
||||
//
|
||||
// An account is not required to have Mailboxes with any particular roles.
|
||||
//
|
||||
// [RFC6154]: https://www.rfc-editor.org/rfc/rfc6154.html
|
||||
// [RFC8457]: https://www.rfc-editor.org/rfc/rfc8457.html
|
||||
Role string `json:"role,omitempty"`
|
||||
|
||||
// (default: 0) Defines the sort order of Mailboxes when presented in the client’s UI, so it is consistent between devices.
|
||||
//
|
||||
// The number MUST be an integer in the range 0 <= sortOrder < 2^31.
|
||||
//
|
||||
// A Mailbox with a lower order should be displayed before a Mailbox with a higher order
|
||||
// (that has the same parent) in any Mailbox listing in the client’s UI.
|
||||
// Mailboxes with equal order SHOULD be sorted in alphabetical order by name.
|
||||
// The sorting should take into account locale-specific character order convention.
|
||||
SortOrder int `json:"sortOrder,omitzero"`
|
||||
|
||||
// The number of Emails in this Mailbox.
|
||||
TotalEmails int `json:"totalEmails"`
|
||||
|
||||
// The number of Emails in this Mailbox that have neither the $seen keyword nor the $draft keyword.
|
||||
UnreadEmails int `json:"unreadEmails"`
|
||||
|
||||
// The number of Threads where at least one Email in the Thread is in this Mailbox.
|
||||
TotalThreads int `json:"totalThreads"`
|
||||
|
||||
// An indication of the number of “unread” Threads in the Mailbox.
|
||||
UnreadThreads int `json:"unreadThreads"`
|
||||
|
||||
// The set of rights (Access Control Lists (ACLs)) the user has in relation to this Mailbox.
|
||||
//
|
||||
// These are backwards compatible with IMAP ACLs, as defined in [RFC4314].
|
||||
//
|
||||
// [RFC4314]: https://www.rfc-editor.org/rfc/rfc4314.html
|
||||
MyRights MailboxRights `json:"myRights,omitempty"`
|
||||
|
||||
// Has the user indicated they wish to see this Mailbox in their client?
|
||||
//
|
||||
// This SHOULD default to false for Mailboxes in shared accounts the user has access to and true
|
||||
// for any new Mailboxes created by the user themself.
|
||||
//
|
||||
// This MUST be stored separately per user where multiple users have access to a shared Mailbox.
|
||||
//
|
||||
// A user may have permission to access a large number of shared accounts, or a shared account with a very
|
||||
// large set of Mailboxes, but only be interested in the contents of a few of these.
|
||||
//
|
||||
// Clients may choose to only display Mailboxes where the isSubscribed property is set to true, and offer
|
||||
// a separate UI to allow the user to see and subscribe/unsubscribe from the full set of Mailboxes.
|
||||
//
|
||||
// However, clients MAY choose to ignore this property, either entirely for ease of implementation or just
|
||||
// for an account where isPersonal is true (indicating it is the user’s own rather than a shared account).
|
||||
//
|
||||
// This property corresponds to IMAP [RFC3501] Mailbox subscriptions.
|
||||
//
|
||||
// [RFC3501]: https://www.rfc-editor.org/rfc/rfc3501.html
|
||||
IsSubscribed bool `json:"isSubscribed"`
|
||||
}
|
||||
|
||||
type MailboxGetCommand struct {
|
||||
@@ -1199,6 +1333,13 @@ type EmailSubmissionSetResponse struct {
|
||||
// TODO(pbleser-oc) add updated and destroyed when they are needed
|
||||
}
|
||||
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
VacationResponseType ObjectType = "VacationResponse"
|
||||
EmailType ObjectType = "Email"
|
||||
)
|
||||
|
||||
type Command string
|
||||
|
||||
type Invocation struct {
|
||||
@@ -1424,8 +1565,8 @@ type MailboxQueryResponse struct {
|
||||
}
|
||||
|
||||
type EmailBodyStructure struct {
|
||||
Type string //`json:"type"`
|
||||
PartId string //`json:"partId"`
|
||||
Type string `json:"type"`
|
||||
PartId string `json:"partId"`
|
||||
Other map[string]any `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
@@ -1579,7 +1720,7 @@ type VacationResponseGetCommand struct {
|
||||
type VacationResponse struct {
|
||||
// The id of the object.
|
||||
// There is only ever one VacationResponse object, and its id is "singleton"
|
||||
Id string `json:"id"`
|
||||
Id string `json:"id,omitempty"`
|
||||
// Should a vacation response be sent if a message arrives between the "fromDate" and "toDate"?
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
// If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the
|
||||
@@ -1616,7 +1757,7 @@ type VacationResponseGetResponse struct {
|
||||
NotFound []any `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
type VacationResponseSetRequest struct {
|
||||
type VacationResponseSetCommand struct {
|
||||
AccountId string `json:"accountId"`
|
||||
IfInState string `json:"ifInState,omitempty"`
|
||||
Create map[string]VacationResponse `json:"create,omitempty"`
|
||||
|
||||
@@ -111,10 +111,9 @@ func (s *Session) SubmissionAccountId(accountId string) string {
|
||||
}
|
||||
|
||||
// Create a new log.Logger that is decorated with fields containing information about the Session.
|
||||
func (s Session) DecorateLogger(l log.Logger) log.Logger {
|
||||
return log.Logger{Logger: l.With().
|
||||
func (s Session) DecorateLogger(l log.Logger) *log.Logger {
|
||||
return log.From(l.With().
|
||||
Str(logUsername, s.Username).
|
||||
Str(logApiUrl, s.ApiUrl).
|
||||
Str(logSessionState, s.State).
|
||||
Logger()}
|
||||
Str(logSessionState, s.State))
|
||||
}
|
||||
|
||||
@@ -107,8 +107,9 @@ func serveTestFile(t *testing.T, name string) ([]byte, Error) {
|
||||
err = json.Unmarshal(bytes, &target)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse JSON test data file '%v': %v", p, err)
|
||||
return nil, SimpleError{code: 0, err: err}
|
||||
}
|
||||
return bytes, SimpleError{code: 0, err: err}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
|
||||
@@ -152,7 +153,7 @@ func TestRequests(t *testing.T) {
|
||||
|
||||
folders, err := client.GetAllMailboxes("a", &session, ctx, &logger)
|
||||
require.NoError(err)
|
||||
require.Len(folders.List, 5)
|
||||
require.Len(folders.Mailboxes, 5)
|
||||
|
||||
emails, err := client.GetAllEmails("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0)
|
||||
require.NoError(err)
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type eventListeners[T any] struct {
|
||||
@@ -154,6 +154,14 @@ func retrieveResponseMatchParameters[T any](data *Response, command Command, tag
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e EmailBodyStructure) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{}
|
||||
maps.Copy(m, e.Other) // do this first to avoid overwriting type and partId
|
||||
m["type"] = e.Type
|
||||
m["partId"] = e.PartId
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (e *EmailBodyStructure) UnmarshalJSON(bs []byte) error {
|
||||
m := map[string]any{}
|
||||
err := json.Unmarshal(bs, &m)
|
||||
@@ -163,16 +171,6 @@ func (e *EmailBodyStructure) UnmarshalJSON(bs []byte) error {
|
||||
return decodeMap(m, e)
|
||||
}
|
||||
|
||||
func (e *EmailBodyStructure) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{}
|
||||
m["type"] = e.Type
|
||||
m["partId"] = e.PartId
|
||||
for k, v := range e.Other {
|
||||
m[k] = v
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (i *Invocation) MarshalJSON() ([]byte, error) {
|
||||
// JMAP requests have a slightly unusual structure since they are not a JSON object
|
||||
// but, instead, a three-element array composed of
|
||||
@@ -223,47 +221,3 @@ func (i *Invocation) UnmarshalJSON(bs []byte) error {
|
||||
i.Parameters = params
|
||||
return nil
|
||||
}
|
||||
|
||||
const logMaxStrLength = 1024
|
||||
|
||||
// Safely caps a string to a given size to avoid log bombing.
|
||||
// Use this function to wrap strings that are user input (HTTP headers, path parameters, URI parameters, HTTP body, ...).
|
||||
func logstr(text string) string {
|
||||
runes := []rune(text)
|
||||
|
||||
if len(runes) <= logMaxStrLength {
|
||||
return text
|
||||
} else {
|
||||
return string(runes[0:logMaxStrLength-1]) + `\u2026` // hellip
|
||||
}
|
||||
}
|
||||
|
||||
type SafeLogStringArrayMarshaller struct {
|
||||
array []string
|
||||
}
|
||||
|
||||
func (m SafeLogStringArrayMarshaller) MarshalZerologArray(a *zerolog.Array) {
|
||||
for _, elem := range m.array {
|
||||
a.Str(logstr(elem))
|
||||
}
|
||||
}
|
||||
|
||||
var _ zerolog.LogArrayMarshaler = SafeLogStringArrayMarshaller{}
|
||||
|
||||
func logstrarray(array []string) SafeLogStringArrayMarshaller {
|
||||
return SafeLogStringArrayMarshaller{array: array}
|
||||
}
|
||||
|
||||
func uniq[T comparable](ary []T) []T {
|
||||
m := map[T]bool{}
|
||||
for _, v := range ary {
|
||||
m[v] = true
|
||||
}
|
||||
set := make([]T, len(m))
|
||||
i := 0
|
||||
for v := range m {
|
||||
set[i] = v
|
||||
i++
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ func TestDeserializeMailboxGetResponse(t *testing.T) {
|
||||
require.Len(mgr.List, 5)
|
||||
require.Equal("n", mgr.State)
|
||||
require.Empty(mgr.NotFound)
|
||||
var rights = []string{"mayReadItems", "mayAddItems", "mayRemoveItems", "maySetSeen", "maySetKeywords", "mayCreateChild", "mayRename", "mayDelete", "maySubmit"}
|
||||
var folders = []struct {
|
||||
id string
|
||||
name string
|
||||
@@ -53,10 +52,15 @@ func TestDeserializeMailboxGetResponse(t *testing.T) {
|
||||
require.Zero(folder.SortOrder)
|
||||
require.True(folder.IsSubscribed)
|
||||
|
||||
for _, right := range rights {
|
||||
require.Contains(folder.MyRights, right)
|
||||
require.True(folder.MyRights[right])
|
||||
}
|
||||
require.True(folder.MyRights.MayReadItems)
|
||||
require.True(folder.MyRights.MayAddItems)
|
||||
require.True(folder.MyRights.MayRemoveItems)
|
||||
require.True(folder.MyRights.MaySetSeen)
|
||||
require.True(folder.MyRights.MaySetKeywords)
|
||||
require.True(folder.MyRights.MayCreateChild)
|
||||
require.True(folder.MyRights.MayRename)
|
||||
require.True(folder.MyRights.MayDelete)
|
||||
require.True(folder.MyRights.MaySubmit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ func TestDeserializeEmailGetResponse(t *testing.T) {
|
||||
require.Equal("cbejozsk1fgcviw7thwzsvtgmf1ep0a3izjoimj02jmtsunpeuwmsaya1yma", email.BlobId)
|
||||
}
|
||||
|
||||
func TestUnknown(t *testing.T) {
|
||||
func TestUnmarshallingUnknown(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
const text = `{
|
||||
@@ -109,8 +113,24 @@ func TestUnknown(t *testing.T) {
|
||||
require.Equal(bs.Other["header:x"], "yz")
|
||||
require.Contains(bs.Other, "header:a")
|
||||
require.Equal(bs.Other["header:a"], "bc")
|
||||
|
||||
result, err := json.Marshal(target)
|
||||
require.NoError(err)
|
||||
require.Equal(`{"subject":"aaa","bodyStructure":{"type":"a","partId":"b","header:a":"bc","header:x":"yz"}}`, string(result))
|
||||
}
|
||||
|
||||
func TestMarshallingUnknown(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
source := EmailCreate{
|
||||
Subject: "aaa",
|
||||
BodyStructure: EmailBodyStructure{
|
||||
Type: "a",
|
||||
PartId: "b",
|
||||
Other: map[string]any{
|
||||
"header:x": "yz",
|
||||
"header:a": "bc",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := json.Marshal(source)
|
||||
require.NoError(err)
|
||||
require.Equal(`{"subject":"aaa","bodyStructure":{"header:a":"bc","header:x":"yz","partId":"b","type":"a"}}`, string(result))
|
||||
}
|
||||
|
||||
43
pkg/log/log_safely.go
Normal file
43
pkg/log/log_safely.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package log
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
const (
|
||||
logMaxStrLength = 512
|
||||
logMaxStrArrayLength = 16 // 8kb
|
||||
)
|
||||
|
||||
// Safely caps a string to a given size to avoid log bombing.
|
||||
// Use this function to wrap strings that are user input (HTTP headers, path parameters, URI parameters, HTTP body, ...).
|
||||
func SafeString(text string) string {
|
||||
runes := []rune(text)
|
||||
|
||||
if len(runes) <= logMaxStrLength {
|
||||
return text
|
||||
} else {
|
||||
return string(runes[0:logMaxStrLength-1]) + `\u2026` // hellip
|
||||
}
|
||||
}
|
||||
|
||||
type SafeLogStringArrayMarshaller struct {
|
||||
array []string
|
||||
}
|
||||
|
||||
func (m SafeLogStringArrayMarshaller) MarshalZerologArray(a *zerolog.Array) {
|
||||
for i, elem := range m.array {
|
||||
if i >= logMaxStrArrayLength {
|
||||
return
|
||||
}
|
||||
a.Str(SafeString(elem))
|
||||
}
|
||||
}
|
||||
|
||||
var _ zerolog.LogArrayMarshaler = SafeLogStringArrayMarshaller{}
|
||||
|
||||
func SafeStringArray(array []string) SafeLogStringArrayMarshaller {
|
||||
return SafeLogStringArrayMarshaller{array: array}
|
||||
}
|
||||
|
||||
func From(context zerolog.Context) *Logger {
|
||||
return &Logger{Logger: context.Logger()}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
// Package structs provides some utility functions for dealing with structs.
|
||||
package structs
|
||||
|
||||
import (
|
||||
orderedmap "github.com/wk8/go-ordered-map"
|
||||
)
|
||||
|
||||
// CopyOrZeroValue returns a copy of s if s is not nil otherwise the zero value of T will be returned.
|
||||
func CopyOrZeroValue[T any](s *T) *T {
|
||||
cp := new(T)
|
||||
@@ -9,3 +13,20 @@ func CopyOrZeroValue[T any](s *T) *T {
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
// Returns a copy of an array with a unique set of elements.
|
||||
//
|
||||
// Element order is retained.
|
||||
func Uniq[T comparable](source []T) []T {
|
||||
m := orderedmap.New()
|
||||
for _, v := range source {
|
||||
m.Set(v, true)
|
||||
}
|
||||
set := make([]T, m.Len())
|
||||
i := 0
|
||||
for pair := m.Oldest(); pair != nil; pair = pair.Next() {
|
||||
set[i] = pair.Key.(T)
|
||||
i++
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package structs
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type example struct {
|
||||
Attribute1 string
|
||||
@@ -36,3 +41,45 @@ func TestCopyOrZeroValue(t *testing.T) {
|
||||
t.Error("CopyOrZeroValue didn't correctly copy attributes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqWithInts(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []int
|
||||
expected []int
|
||||
}{
|
||||
{[]int{5, 1, 3, 1, 4}, []int{5, 1, 3, 4}},
|
||||
{[]int{1, 1, 1}, []int{1}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) {
|
||||
result := Uniq(tt.input)
|
||||
assert.EqualValues(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type u struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
|
||||
var (
|
||||
u1 = u{x: 1, y: "un"}
|
||||
u2 = u{x: 2, y: "deux"}
|
||||
u3 = u{x: 3, y: "trois"}
|
||||
)
|
||||
|
||||
func TestUniqWithStructs(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []u
|
||||
expected []u
|
||||
}{
|
||||
{[]u{u3, u1, u2, u3, u2, u1}, []u{u3, u1, u2}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) {
|
||||
result := Uniq(tt.input)
|
||||
assert.EqualValues(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,33 +11,3 @@ const (
|
||||
var Capabilities = []string{
|
||||
CapMail_1,
|
||||
}
|
||||
|
||||
// When the request contains invalid parameters.
|
||||
// swagger:response ErrorResponse400
|
||||
type SwaggerErrorResponse400 struct {
|
||||
// in: body
|
||||
Body struct {
|
||||
*ErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// When the requested object does not exist.
|
||||
// swagger:response ErrorResponse404
|
||||
type SwaggerErrorResponse404 struct {
|
||||
}
|
||||
|
||||
// When the server was unable to complete the request.
|
||||
// swagger:response ErrorResponse500
|
||||
type SwaggerErrorResponse500 struct {
|
||||
// in: body
|
||||
Body struct {
|
||||
*ErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:parameters vacation mailboxes
|
||||
type SwaggerAccountParams struct {
|
||||
// The identifier of the account.
|
||||
// in: path
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ func (g Groupware) GetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return etagOnlyResponse(res, res.Digest())
|
||||
blob := res.Blob
|
||||
if blob == nil {
|
||||
return notFoundResponse("")
|
||||
}
|
||||
return etagOnlyResponse(res, blob.Digest())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
type IndexLimits struct {
|
||||
@@ -79,7 +80,7 @@ func (g Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
accountIds[i] = k
|
||||
i++
|
||||
}
|
||||
accountIds = uniq(accountIds)
|
||||
accountIds = structs.Uniq(accountIds)
|
||||
|
||||
identitiesResponse, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -44,10 +44,10 @@ func (g Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
|
||||
if len(res.List) == 1 {
|
||||
return response(res.List[0], res.State)
|
||||
if len(res.Mailboxes) == 1 {
|
||||
return etagResponse(res.Mailboxes[0], res.SessionState, res.State)
|
||||
} else {
|
||||
return notFoundResponse(res.State)
|
||||
return notFoundResponse(res.SessionState)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -92,17 +92,17 @@ func (g Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
var filter jmap.MailboxFilterCondition
|
||||
|
||||
hasCriteria := false
|
||||
name := q.Get("name")
|
||||
name := q.Get(QueryParamMailboxSearchName)
|
||||
if name != "" {
|
||||
filter.Name = name
|
||||
hasCriteria = true
|
||||
}
|
||||
role := q.Get("role")
|
||||
role := q.Get(QueryParamMailboxSearchRole)
|
||||
if role != "" {
|
||||
filter.Role = role
|
||||
hasCriteria = true
|
||||
}
|
||||
subscribed := q.Get("subscribed")
|
||||
subscribed := q.Get(QueryParamMailboxSearchSubscribed)
|
||||
if subscribed != "" {
|
||||
b, err := strconv.ParseBool(subscribed)
|
||||
if err != nil {
|
||||
@@ -120,13 +120,13 @@ func (g Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return response(mailboxes.Mailboxes, mailboxes.State)
|
||||
return etagResponse(mailboxes.Mailboxes, mailboxes.SessionState, mailboxes.State)
|
||||
} else {
|
||||
mailboxes, err := g.jmap.GetAllMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return response(mailboxes.List, mailboxes.State)
|
||||
return etagResponse(mailboxes.Mailboxes, mailboxes.SessionState, mailboxes.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (g Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reques
|
||||
withSource(&ErrorSource{Parameter: UriParamMailboxId}),
|
||||
))
|
||||
}
|
||||
logger := &log.Logger{Logger: req.logger.With().Str(HeaderSince, since).Logger()}
|
||||
logger := log.From(req.logger.With().Str(HeaderSince, since))
|
||||
|
||||
emails, jerr := g.jmap.GetEmailsInMailboxSince(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
@@ -64,7 +64,7 @@ func (g Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reques
|
||||
l = l.Int(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
logger := &log.Logger{Logger: l.Logger()}
|
||||
logger := log.From(l)
|
||||
|
||||
emails, jerr := g.jmap.GetAllEmails(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
@@ -82,14 +82,14 @@ func (g Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) {
|
||||
ids := strings.Split(id, ",")
|
||||
if len(ids) < 1 {
|
||||
errorId := req.errorId()
|
||||
msg := fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamMessageId, logstr(id), "empty list of mail ids")
|
||||
msg := fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamMessageId, log.SafeString(id), "empty list of mail ids")
|
||||
return errorResponse(apiError(errorId, ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: UriParamMessageId}),
|
||||
))
|
||||
}
|
||||
|
||||
logger := &log.Logger{Logger: req.logger.With().Str("id", logstr(id)).Logger()}
|
||||
logger := log.From(req.logger.With().Str("id", log.SafeString(id)))
|
||||
emails, jerr := g.jmap.GetEmails(req.GetAccountId(), req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
@@ -109,7 +109,7 @@ func (g Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, sinc
|
||||
if ok {
|
||||
l = l.Int(QueryParamMaxChanges, maxChanges)
|
||||
}
|
||||
logger := &log.Logger{Logger: l.Logger()}
|
||||
logger := log.From(l)
|
||||
|
||||
emails, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
@@ -192,31 +192,31 @@ func (g Groupware) buildQuery(req Request) (bool, jmap.EmailFilterElement, int,
|
||||
}
|
||||
|
||||
if mailboxId != "" {
|
||||
l = l.Str(QueryParamMailboxId, logstr(mailboxId))
|
||||
l = l.Str(QueryParamMailboxId, log.SafeString(mailboxId))
|
||||
}
|
||||
if len(notInMailboxIds) > 0 {
|
||||
l = l.Array(QueryParamNotInMailboxId, logstrarray(notInMailboxIds))
|
||||
l = l.Array(QueryParamNotInMailboxId, log.SafeStringArray(notInMailboxIds))
|
||||
}
|
||||
if text != "" {
|
||||
l = l.Str(QueryParamSearchText, logstr(text))
|
||||
l = l.Str(QueryParamSearchText, log.SafeString(text))
|
||||
}
|
||||
if from != "" {
|
||||
l = l.Str(QueryParamSearchFrom, logstr(from))
|
||||
l = l.Str(QueryParamSearchFrom, log.SafeString(from))
|
||||
}
|
||||
if to != "" {
|
||||
l = l.Str(QueryParamSearchTo, logstr(to))
|
||||
l = l.Str(QueryParamSearchTo, log.SafeString(to))
|
||||
}
|
||||
if cc != "" {
|
||||
l = l.Str(QueryParamSearchCc, logstr(cc))
|
||||
l = l.Str(QueryParamSearchCc, log.SafeString(cc))
|
||||
}
|
||||
if bcc != "" {
|
||||
l = l.Str(QueryParamSearchBcc, logstr(bcc))
|
||||
l = l.Str(QueryParamSearchBcc, log.SafeString(bcc))
|
||||
}
|
||||
if subject != "" {
|
||||
l = l.Str(QueryParamSearchSubject, logstr(subject))
|
||||
l = l.Str(QueryParamSearchSubject, log.SafeString(subject))
|
||||
}
|
||||
if body != "" {
|
||||
l = l.Str(QueryParamSearchBody, logstr(body))
|
||||
l = l.Str(QueryParamSearchBody, log.SafeString(body))
|
||||
}
|
||||
|
||||
minSize, ok, err := req.parseNumericParam(QueryParamSearchMinSize, 0)
|
||||
@@ -235,7 +235,7 @@ func (g Groupware) buildQuery(req Request) (bool, jmap.EmailFilterElement, int,
|
||||
l = l.Int(QueryParamSearchMaxSize, maxSize)
|
||||
}
|
||||
|
||||
logger := &log.Logger{Logger: l.Logger()}
|
||||
logger := log.From(l)
|
||||
|
||||
var filter jmap.EmailFilterElement
|
||||
|
||||
@@ -379,7 +379,7 @@ func (g Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamMessageId, messageId)
|
||||
logger := &log.Logger{Logger: l.Logger()}
|
||||
logger := log.From(l)
|
||||
|
||||
var body MessageCreation
|
||||
err := req.body(&body)
|
||||
|
||||
@@ -40,7 +40,7 @@ func (g Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (g Groupware) SetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
var body jmap.VacationResponseBody
|
||||
var body jmap.VacationResponsePayload
|
||||
err := req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
|
||||
@@ -24,3 +24,33 @@
|
||||
//
|
||||
// swagger:meta
|
||||
package groupware
|
||||
|
||||
// When the request contains invalid parameters.
|
||||
// swagger:response ErrorResponse400
|
||||
type SwaggerErrorResponse400 struct {
|
||||
// in: body
|
||||
Body struct {
|
||||
*ErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// When the requested object does not exist.
|
||||
// swagger:response ErrorResponse404
|
||||
type SwaggerErrorResponse404 struct {
|
||||
}
|
||||
|
||||
// When the server was unable to complete the request.
|
||||
// swagger:response ErrorResponse500
|
||||
type SwaggerErrorResponse500 struct {
|
||||
// in: body
|
||||
Body struct {
|
||||
*ErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:parameters vacation mailboxes
|
||||
type SwaggerAccountParams struct {
|
||||
// The identifier of the account.
|
||||
// in: path
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ const (
|
||||
logQuery = "query"
|
||||
)
|
||||
|
||||
const (
|
||||
logMaxStrLength = 512
|
||||
)
|
||||
|
||||
type Groupware struct {
|
||||
mux *chi.Mux
|
||||
logger *log.Logger
|
||||
@@ -256,7 +252,7 @@ func (r Request) GetAccount() (jmap.SessionAccount, *Error) {
|
||||
errorId := r.errorId()
|
||||
r.logger.Debug().Msgf("failed to find account '%v'", accountId)
|
||||
return jmap.SessionAccount{}, apiError(errorId, ErrorNonExistingAccount,
|
||||
withDetail(fmt.Sprintf("The account '%v' does not exist", logstr(accountId))),
|
||||
withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))),
|
||||
withSource(&ErrorSource{Parameter: UriParamAccount}),
|
||||
)
|
||||
}
|
||||
@@ -277,7 +273,7 @@ func (r Request) parseNumericParam(param string, defaultValue int) (int, bool, *
|
||||
value, err := strconv.ParseInt(str, 10, 0)
|
||||
if err != nil {
|
||||
errorId := r.errorId()
|
||||
msg := fmt.Sprintf("Invalid value for query parameter '%v': '%s': %s", param, logstr(str), err.Error())
|
||||
msg := fmt.Sprintf("Invalid value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return defaultValue, true, apiError(errorId, ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
@@ -300,7 +296,7 @@ func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
|
||||
t, err := time.Parse(time.RFC3339, str)
|
||||
if err != nil {
|
||||
errorId := r.errorId()
|
||||
msg := fmt.Sprintf("Invalid RFC3339 value for query parameter '%v': '%s': %s", param, logstr(str), err.Error())
|
||||
msg := fmt.Sprintf("Invalid RFC3339 value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return time.Time{}, true, apiError(errorId, ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
@@ -323,7 +319,7 @@ func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *E
|
||||
b, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
errorId := r.errorId()
|
||||
msg := fmt.Sprintf("Invalid boolean value for query parameter '%v': '%s': %s", param, logstr(str), err.Error())
|
||||
msg := fmt.Sprintf("Invalid boolean value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return defaultValue, true, apiError(errorId, ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
@@ -348,34 +344,6 @@ func (r Request) body(target any) *Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Safely caps a string to a given size to avoid log bombing.
|
||||
// Use this function to wrap strings that are user input (HTTP headers, path parameters, URI parameters, HTTP body, ...).
|
||||
func logstr(text string) string {
|
||||
runes := []rune(text)
|
||||
|
||||
if len(runes) <= logMaxStrLength {
|
||||
return text
|
||||
} else {
|
||||
return string(runes[0:logMaxStrLength-1]) + `\u2026` // hellip
|
||||
}
|
||||
}
|
||||
|
||||
type SafeLogStringArrayMarshaller struct {
|
||||
array []string
|
||||
}
|
||||
|
||||
func (m SafeLogStringArrayMarshaller) MarshalZerologArray(a *zerolog.Array) {
|
||||
for _, elem := range m.array {
|
||||
a.Str(logstr(elem))
|
||||
}
|
||||
}
|
||||
|
||||
var _ zerolog.LogArrayMarshaler = SafeLogStringArrayMarshaller{}
|
||||
|
||||
func logstrarray(array []string) SafeLogStringArrayMarshaller {
|
||||
return SafeLogStringArrayMarshaller{array: array}
|
||||
}
|
||||
|
||||
func (g Groupware) log(error *Error) {
|
||||
var level *zerolog.Event
|
||||
if error.NumStatus < 300 {
|
||||
@@ -397,13 +365,13 @@ func (g Groupware) log(error *Error) {
|
||||
l := level.Str(logErrorCode, error.Code).Str(logErrorId, error.Id).Int(logErrorStatus, error.NumStatus)
|
||||
if error.Source != nil {
|
||||
if error.Source.Header != "" {
|
||||
l.Str(logErrorSourceHeader, logstr(error.Source.Header))
|
||||
l.Str(logErrorSourceHeader, log.SafeString(error.Source.Header))
|
||||
}
|
||||
if error.Source.Parameter != "" {
|
||||
l.Str(logErrorSourceParameter, logstr(error.Source.Parameter))
|
||||
l.Str(logErrorSourceParameter, log.SafeString(error.Source.Parameter))
|
||||
}
|
||||
if error.Source.Pointer != "" {
|
||||
l.Str(logErrorSourcePointer, logstr(error.Source.Pointer))
|
||||
l.Str(logErrorSourcePointer, log.SafeString(error.Source.Pointer))
|
||||
}
|
||||
}
|
||||
l.Msg(error.Title)
|
||||
@@ -422,9 +390,10 @@ func (g Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Err
|
||||
|
||||
func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) {
|
||||
ctx := r.Context()
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
sl := g.logger.SubloggerWithRequestID(ctx)
|
||||
logger := &sl
|
||||
|
||||
username, ok, err := g.usernameProvider.GetUsername(r, ctx, &logger)
|
||||
username, ok, err := g.usernameProvider.GetUsername(r, ctx, logger)
|
||||
if err != nil {
|
||||
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication))
|
||||
return
|
||||
@@ -434,9 +403,9 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func(
|
||||
return
|
||||
}
|
||||
|
||||
logger = log.Logger{Logger: logger.With().Str(logUsername, logstr(username)).Logger()}
|
||||
logger = log.From(logger.With().Str(logUsername, log.SafeString(username)))
|
||||
|
||||
session, ok, err := g.session(username, r, ctx, &logger)
|
||||
session, ok, err := g.session(username, r, ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session")
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
@@ -448,12 +417,12 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func(
|
||||
render.Status(r, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
logger = session.DecorateLogger(logger)
|
||||
decoratedLogger := session.DecorateLogger(*logger)
|
||||
|
||||
req := Request{
|
||||
r: r,
|
||||
ctx: ctx,
|
||||
logger: &logger,
|
||||
logger: decoratedLogger,
|
||||
session: &session,
|
||||
}
|
||||
|
||||
@@ -489,9 +458,10 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func(
|
||||
|
||||
func (g Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(r Request, w http.ResponseWriter) *Error) {
|
||||
ctx := r.Context()
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
sl := g.logger.SubloggerWithRequestID(ctx)
|
||||
logger := &sl
|
||||
|
||||
username, ok, err := g.usernameProvider.GetUsername(r, ctx, &logger)
|
||||
username, ok, err := g.usernameProvider.GetUsername(r, ctx, logger)
|
||||
if err != nil {
|
||||
g.serveError(w, r, apiError(errorId(r, ctx), ErrorInvalidAuthentication))
|
||||
return
|
||||
@@ -501,9 +471,9 @@ func (g Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(r
|
||||
return
|
||||
}
|
||||
|
||||
logger = log.Logger{Logger: logger.With().Str(logUsername, logstr(username)).Logger()}
|
||||
logger = log.From(logger.With().Str(logUsername, log.SafeString(username)))
|
||||
|
||||
session, ok, err := g.session(username, r, ctx, &logger)
|
||||
session, ok, err := g.session(username, r, ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session")
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
@@ -515,12 +485,12 @@ func (g Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(r
|
||||
render.Status(r, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
logger = session.DecorateLogger(logger)
|
||||
decoratedLogger := session.DecorateLogger(*logger)
|
||||
|
||||
req := Request{
|
||||
r: r,
|
||||
ctx: ctx,
|
||||
logger: &logger,
|
||||
logger: decoratedLogger,
|
||||
session: &session,
|
||||
}
|
||||
|
||||
@@ -534,57 +504,12 @@ func (g Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(r
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func (g Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r Request) (any, string, error)) (any, string, error) {
|
||||
ctx := r.Context()
|
||||
logger := g.logger.SubloggerWithRequestID(ctx)
|
||||
session, ok, err := g.session(r, ctx, &logger)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session")
|
||||
return nil, "", err
|
||||
}
|
||||
if !ok {
|
||||
// no session = authentication failed
|
||||
logger.Warn().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not authenticate")
|
||||
return nil, "", err
|
||||
}
|
||||
logger = session.DecorateLogger(logger)
|
||||
|
||||
req := Request{
|
||||
r: r,
|
||||
ctx: ctx,
|
||||
logger: &logger,
|
||||
session: &session,
|
||||
}
|
||||
|
||||
response, state, err := handler(req)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg(err.Error())
|
||||
}
|
||||
return response, state, err
|
||||
}
|
||||
*/
|
||||
|
||||
func (g Groupware) NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
level := g.logger.Debug()
|
||||
if level.Enabled() {
|
||||
path := logstr(r.URL.Path)
|
||||
path := log.SafeString(r.URL.Path)
|
||||
level.Str("path", path).Int(logErrorStatus, http.StatusNotFound).Msgf("unmatched path: '%v'", path)
|
||||
}
|
||||
render.Status(r, http.StatusNotFound)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func uniq[T comparable](ary []T) []T {
|
||||
m := map[T]bool{}
|
||||
for _, v := range ary {
|
||||
m[v] = true
|
||||
}
|
||||
set := make([]T, len(m))
|
||||
i := 0
|
||||
for v := range m {
|
||||
set[i] = v
|
||||
i++
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -5,33 +5,36 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
UriParamAccount = "accountid"
|
||||
UriParamMailboxId = "mailbox"
|
||||
UriParamMessageId = "messageid"
|
||||
UriParamBlobId = "blobid"
|
||||
UriParamBlobName = "blobname"
|
||||
QueryParamBlobType = "type"
|
||||
QueryParamSince = "since"
|
||||
QueryParamMaxChanges = "maxchanges"
|
||||
QueryParamMailboxId = "mailbox"
|
||||
QueryParamNotInMailboxId = "notmailbox"
|
||||
QueryParamSearchText = "text"
|
||||
QueryParamSearchFrom = "from"
|
||||
QueryParamSearchTo = "to"
|
||||
QueryParamSearchCc = "cc"
|
||||
QueryParamSearchBcc = "bcc"
|
||||
QueryParamSearchSubject = "subject"
|
||||
QueryParamSearchBody = "body"
|
||||
QueryParamSearchBefore = "before"
|
||||
QueryParamSearchAfter = "after"
|
||||
QueryParamSearchMinSize = "minsize"
|
||||
QueryParamSearchMaxSize = "maxsize"
|
||||
QueryParamSearchKeyword = "keyword"
|
||||
QueryParamSearchFetchBodies = "fetchbodies"
|
||||
QueryParamSearchFetchEmails = "fetchemails"
|
||||
QueryParamOffset = "offset"
|
||||
QueryParamLimit = "limit"
|
||||
HeaderSince = "if-none-match"
|
||||
UriParamAccount = "accountid"
|
||||
UriParamMailboxId = "mailbox"
|
||||
UriParamMessageId = "messageid"
|
||||
UriParamBlobId = "blobid"
|
||||
UriParamBlobName = "blobname"
|
||||
QueryParamMailboxSearchName = "name"
|
||||
QueryParamMailboxSearchRole = "role"
|
||||
QueryParamMailboxSearchSubscribed = "subscribed"
|
||||
QueryParamBlobType = "type"
|
||||
QueryParamSince = "since"
|
||||
QueryParamMaxChanges = "maxchanges"
|
||||
QueryParamMailboxId = "mailbox"
|
||||
QueryParamNotInMailboxId = "notmailbox"
|
||||
QueryParamSearchText = "text"
|
||||
QueryParamSearchFrom = "from"
|
||||
QueryParamSearchTo = "to"
|
||||
QueryParamSearchCc = "cc"
|
||||
QueryParamSearchBcc = "bcc"
|
||||
QueryParamSearchSubject = "subject"
|
||||
QueryParamSearchBody = "body"
|
||||
QueryParamSearchBefore = "before"
|
||||
QueryParamSearchAfter = "after"
|
||||
QueryParamSearchMinSize = "minsize"
|
||||
QueryParamSearchMaxSize = "maxsize"
|
||||
QueryParamSearchKeyword = "keyword"
|
||||
QueryParamSearchFetchBodies = "fetchbodies"
|
||||
QueryParamSearchFetchEmails = "fetchemails"
|
||||
QueryParamOffset = "offset"
|
||||
QueryParamLimit = "limit"
|
||||
HeaderSince = "if-none-match"
|
||||
)
|
||||
|
||||
func (g Groupware) Route(r chi.Router) {
|
||||
@@ -41,7 +44,7 @@ func (g Groupware) Route(r chi.Router) {
|
||||
r.Get("/", g.GetAccount)
|
||||
r.Get("/identities", g.GetIdentities)
|
||||
r.Get("/vacation", g.GetVacation)
|
||||
r.Post("/vacation", g.SetVacation)
|
||||
r.Put("/vacation", g.SetVacation)
|
||||
r.Route("/mailboxes", func(r chi.Router) {
|
||||
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
|
||||
r.Get("/{mailbox}", g.GetMailbox)
|
||||
|
||||
Reference in New Issue
Block a user