From f2106ec809e9888e57f83bd3d5050ae4e02e596f Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Thu, 7 Aug 2025 13:53:59 +0200 Subject: [PATCH] groupware: fix email search, add variant that includes the full emails --- pkg/jmap/jmap_api_email.go | 125 +++++- pkg/jmap/jmap_model.go | 8 +- pkg/jmap/jmap_test.go | 65 +++ .../pkg/groupware/groupware_api_messages.go | 376 ++++++++++++------ .../pkg/groupware/groupware_error.go | 7 + .../pkg/groupware/groupware_framework.go | 28 +- .../pkg/groupware/groupware_route.go | 52 +-- 7 files changed, 498 insertions(+), 163 deletions(-) diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index b3a4d98eb4..07faa9f04f 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -245,7 +245,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context. }) } -type EmailQueryResult struct { +type EmailSnippetQueryResult struct { Snippets []SearchSnippet `json:"snippets,omitempty"` QueryState string `json:"queryState"` Total int `json:"total"` @@ -254,10 +254,10 @@ type EmailQueryResult struct { SessionState string `json:"sessionState,omitempty"` } -func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (EmailQueryResult, Error) { +func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int) (EmailSnippetQueryResult, Error) { aid := session.MailAccountId(accountId) logger = j.loggerParams(aid, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { - return z.Bool(logFetchBodies, fetchBodies) + return z.Int(logLimit, limit).Int(logOffset, offset) }) query := EmailQueryCommand{ @@ -289,6 +289,96 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio invocation(SearchSnippetGet, snippet, "1"), ) + if err != nil { + return EmailSnippetQueryResult{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err} + } + + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailSnippetQueryResult, Error) { + var queryResponse EmailQueryResponse + err = retrieveResponseMatchParameters(body, EmailQuery, "0", &queryResponse) + if err != nil { + return EmailSnippetQueryResult{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} + } + + var snippetResponse SearchSnippetGetResponse + err = retrieveResponseMatchParameters(body, SearchSnippetGet, "1", &snippetResponse) + if err != nil { + return EmailSnippetQueryResult{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} + } + + return EmailSnippetQueryResult{ + Snippets: snippetResponse.List, + QueryState: queryResponse.QueryState, + Total: queryResponse.Total, + Limit: queryResponse.Limit, + Position: queryResponse.Position, + SessionState: body.SessionState, + }, nil + }) + +} + +type EmailWithSnippets struct { + Email Email `json:"email"` + Snippets []SearchSnippet `json:"snippets,omitempty"` +} + +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"` + SessionState string `json:"sessionState,omitempty"` +} + +func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (EmailQueryResult, Error) { + aid := session.MailAccountId(accountId) + logger = j.loggerParams(aid, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { + return z.Bool(logFetchBodies, fetchBodies) + }) + + query := EmailQueryCommand{ + AccountId: aid, + Filter: filter, + Sort: []Sort{{Property: emailSortByReceivedAt, IsAscending: false}}, + CollapseThreads: true, + CalculateTotal: true, + } + if offset >= 0 { + query.Position = offset + } + if limit >= 0 { + query.Limit = limit + } + + snippet := SearchSnippetRefCommand{ + AccountId: aid, + Filter: filter, + EmailIdRef: &ResultReference{ + ResultOf: "0", + Name: EmailQuery, + Path: "/ids/*", + }, + } + + mails := EmailGetRefCommand{ + AccountId: aid, + IdRef: &ResultReference{ + ResultOf: "0", + Name: EmailQuery, + Path: "/ids/*", + }, + FetchAllBodyValues: fetchBodies, + MaxBodyValueBytes: maxBodyValueBytes, + } + + cmd, err := request( + invocation(EmailQuery, query, "0"), + invocation(SearchSnippetGet, snippet, "1"), + invocation(EmailGet, mails, "2"), + ) + if err != nil { return EmailQueryResult{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err} } @@ -306,8 +396,35 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio return EmailQueryResult{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} } + var emailsResponse EmailGetResponse + err = retrieveResponseMatchParameters(body, EmailGet, "2", &emailsResponse) + if err != nil { + return EmailQueryResult{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} + } + + snippetsById := map[string][]SearchSnippet{} + for _, snippet := range snippetResponse.List { + list, ok := snippetsById[snippet.EmailId] + if !ok { + list = []SearchSnippet{} + } + snippetsById[snippet.EmailId] = append(list, snippet) + } + + results := []EmailWithSnippets{} + for _, email := range emailsResponse.List { + snippets, ok := snippetsById[email.Id] + if !ok { + snippets = []SearchSnippet{} + } + results = append(results, EmailWithSnippets{ + Email: email, + Snippets: snippets, + }) + } + return EmailQueryResult{ - Snippets: snippetResponse.List, + Results: results, QueryState: queryResponse.QueryState, Total: queryResponse.Total, Limit: queryResponse.Limit, diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 44da27eb31..a59bc8eacf 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -376,7 +376,6 @@ type EmailFilterElement interface { } type EmailFilterCondition struct { - EmailFilterElement InMailbox string `json:"inMailbox,omitempty"` InMailboxOtherThan []string `json:"inMailboxOtherThan,omitempty"` Before time.Time `json:"before,omitzero"` // omitzero requires Go 1.24 @@ -399,14 +398,19 @@ type EmailFilterCondition struct { Header []string `json:"header,omitempty"` } +func (f EmailFilterCondition) _isAnEmailFilterElement() { +} + var _ EmailFilterElement = &EmailFilterCondition{} type EmailFilterOperator struct { - EmailFilterElement Operator FilterOperatorTerm `json:"operator"` Conditions []EmailFilterElement `json:"conditions,omitempty"` } +func (o EmailFilterOperator) _isAnEmailFilterElement() { +} + var _ EmailFilterElement = &EmailFilterOperator{} type Sort struct { diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go index a0c2b7f292..c04e777c69 100644 --- a/pkg/jmap/jmap_test.go +++ b/pkg/jmap/jmap_test.go @@ -169,3 +169,68 @@ func TestRequests(t *testing.T) { require.Equal(false, email.HasAttachment) } } + +func TestEmailFilterSerialization(t *testing.T) { + expectedFilterJson := ` +{"operator":"AND","conditions":[{"hasKeyword":"seen","text":"sample"},{"hasKeyword":"draft"}]} +` + + require := require.New(t) + + text := "sample" + mailboxId := "" + notInMailboxIds := []string{} + from := "" + to := "" + cc := "" + bcc := "" + subject := "" + body := "" + before := time.Time{} + after := time.Time{} + minSize := 0 + maxSize := 0 + keywords := []string{"seen", "draft"} + + var filter EmailFilterElement + + firstFilter := EmailFilterCondition{ + Text: text, + InMailbox: mailboxId, + InMailboxOtherThan: notInMailboxIds, + From: from, + To: to, + Cc: cc, + Bcc: bcc, + Subject: subject, + Body: body, + Before: before, + After: after, + MinSize: minSize, + MaxSize: maxSize, + } + filter = &firstFilter + + if len(keywords) > 0 { + firstFilter.HasKeyword = keywords[0] + if len(keywords) > 1 { + firstFilter.HasKeyword = keywords[0] + filters := make([]EmailFilterElement, len(keywords)) + filters[0] = firstFilter + for i, keyword := range keywords[1:] { + filters[i+1] = EmailFilterCondition{ + HasKeyword: keyword, + } + } + filter = &EmailFilterOperator{ + Operator: And, + Conditions: filters, + } + } + } + + b, err := json.Marshal(filter) + require.NoError(err) + json := string(b) + require.Equal(strings.TrimSpace(expectedFilterJson), json) +} diff --git a/services/groupware/pkg/groupware/groupware_api_messages.go b/services/groupware/pkg/groupware/groupware_api_messages.go index bbd08496e1..9a79521edb 100644 --- a/services/groupware/pkg/groupware/groupware_api_messages.go +++ b/services/groupware/pkg/groupware/groupware_api_messages.go @@ -99,142 +99,258 @@ func (g Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) { }) } +func (g Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, since string) { + g.respond(w, r, func(req Request) Response { + l := req.logger.With().Str(QueryParamSince, since) + maxChanges, ok, err := req.parseNumericParam(QueryParamMaxChanges, -1) + if err != nil { + return errorResponse(err) + } + if ok { + l = l.Int(QueryParamMaxChanges, maxChanges) + } + logger := &log.Logger{Logger: l.Logger()} + + emails, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + + return response(emails, emails.State) + }) +} + +type MessageSearchSnippetsResults struct { + Results []jmap.SearchSnippet `json:"results,omitempty"` + Total int `json:"total,omitzero"` + Limit int `json:"limit,omitzero"` + QueryState string `json:"queryState,omitempty"` +} + +type EmailWithSnippets struct { + jmap.Email + Snippets []SnippetWithoutEmailId `json:"snippets,omitempty"` +} + +type SnippetWithoutEmailId struct { + Subject string `json:"subject,omitempty"` + Preview string `json:"preview,omitempty"` +} + +type MessageSearchResults struct { + Results []EmailWithSnippets `json:"results"` + Total int `json:"total,omitzero"` + Limit int `json:"limit,omitzero"` + QueryState string `json:"queryState,omitempty"` +} + +func (g Groupware) buildQuery(req Request) (bool, jmap.EmailFilterElement, int, int, *log.Logger, Response) { + q := req.r.URL.Query() + mailboxId := q.Get(QueryParamMailboxId) + notInMailboxIds := q[QueryParamNotInMailboxId] + text := q.Get(QueryParamSearchText) + from := q.Get(QueryParamSearchFrom) + to := q.Get(QueryParamSearchTo) + cc := q.Get(QueryParamSearchCc) + bcc := q.Get(QueryParamSearchBcc) + subject := q.Get(QueryParamSearchSubject) + body := q.Get(QueryParamSearchBody) + keywords := q[QueryParamSearchKeyword] + + l := req.logger.With() + + offset, ok, err := req.parseNumericParam(QueryParamOffset, 0) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Int(QueryParamOffset, offset) + } + + limit, ok, err := req.parseNumericParam(QueryParamLimit, g.defaultEmailLimit) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Int(QueryParamLimit, limit) + } + + before, ok, err := req.parseDateParam(QueryParamSearchBefore) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Time(QueryParamSearchBefore, before) + } + + after, ok, err := req.parseDateParam(QueryParamSearchAfter) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Time(QueryParamSearchAfter, after) + } + + if mailboxId != "" { + l = l.Str(QueryParamMailboxId, logstr(mailboxId)) + } + if len(notInMailboxIds) > 0 { + l = l.Array(QueryParamNotInMailboxId, logstrarray(notInMailboxIds)) + } + if text != "" { + l = l.Str(QueryParamSearchText, logstr(text)) + } + if from != "" { + l = l.Str(QueryParamSearchFrom, logstr(from)) + } + if to != "" { + l = l.Str(QueryParamSearchTo, logstr(to)) + } + if cc != "" { + l = l.Str(QueryParamSearchCc, logstr(cc)) + } + if bcc != "" { + l = l.Str(QueryParamSearchBcc, logstr(bcc)) + } + if subject != "" { + l = l.Str(QueryParamSearchSubject, logstr(subject)) + } + if body != "" { + l = l.Str(QueryParamSearchBody, logstr(body)) + } + + minSize, ok, err := req.parseNumericParam(QueryParamSearchMinSize, 0) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Int(QueryParamSearchMinSize, minSize) + } + + maxSize, ok, err := req.parseNumericParam(QueryParamSearchMaxSize, 0) + if err != nil { + return false, nil, 0, 0, nil, errorResponse(err) + } + if ok { + l = l.Int(QueryParamSearchMaxSize, maxSize) + } + + logger := &log.Logger{Logger: l.Logger()} + + var filter jmap.EmailFilterElement + + firstFilter := jmap.EmailFilterCondition{ + Text: text, + InMailbox: mailboxId, + InMailboxOtherThan: notInMailboxIds, + From: from, + To: to, + Cc: cc, + Bcc: bcc, + Subject: subject, + Body: body, + Before: before, + After: after, + MinSize: minSize, + MaxSize: maxSize, + } + filter = &firstFilter + + if len(keywords) > 0 { + firstFilter.HasKeyword = keywords[0] + if len(keywords) > 1 { + firstFilter.HasKeyword = keywords[0] + filters := make([]jmap.EmailFilterElement, len(keywords)-1) + for i, keyword := range keywords[1:] { + filters[i] = jmap.EmailFilterCondition{HasKeyword: keyword} + } + filter = &jmap.EmailFilterOperator{ + Operator: jmap.And, + Conditions: filters, + } + } + } + return true, filter, offset, limit, logger, Response{} +} + +func (g Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + ok, filter, offset, limit, logger, errResp := g.buildQuery(req) + if !ok { + return errResp + } + + fetchEmails, ok, err := req.parseBoolParam(QueryParamSearchFetchEmails, false) + if err != nil { + return errorResponse(err) + } + if ok { + logger = &log.Logger{Logger: logger.With().Bool(QueryParamSearchFetchEmails, fetchEmails).Logger()} + } + + if fetchEmails { + fetchBodies, ok, err := req.parseBoolParam(QueryParamSearchFetchBodies, false) + if err != nil { + return errorResponse(err) + } + if ok { + logger = &log.Logger{Logger: logger.With().Bool(QueryParamSearchFetchBodies, fetchBodies).Logger()} + } + + results, jerr := g.jmap.QueryEmails(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + + flattened := make([]EmailWithSnippets, len(results.Results)) + for i, result := range results.Results { + snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) + for j, snippet := range result.Snippets { + snippets[j] = SnippetWithoutEmailId{ + Subject: snippet.Subject, + Preview: snippet.Preview, + } + } + flattened[i] = EmailWithSnippets{ + Email: result.Email, + Snippets: snippets, + } + } + + return etagResponse(MessageSearchResults{ + Results: flattened, + Total: results.Total, + Limit: results.Limit, + QueryState: results.QueryState, + }, results.SessionState, results.QueryState) + } else { + results, jerr := g.jmap.QueryEmailSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + + return etagResponse(MessageSearchSnippetsResults{ + Results: results.Snippets, + Total: results.Total, + Limit: results.Limit, + QueryState: results.QueryState, + }, results.SessionState, results.QueryState) + } + }) +} + func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() since := q.Get(QueryParamSince) if since == "" { - since = r.Header.Get("If-None-Match") + since = r.Header.Get(HeaderSince) } if since != "" { // get messages changes since a given state - maxChanges := -1 - g.respond(w, r, func(req Request) Response { - logger := &log.Logger{Logger: req.logger.With().Str(HeaderSince, since).Logger()} - - emails, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges) - if jerr != nil { - return req.errorResponseFromJmap(jerr) - } - - return response(emails, emails.State) - }) + g.getMessagesSince(w, r, since) } else { // do a search - g.respond(w, r, func(req Request) Response { - mailboxId := q.Get(QueryParamMailboxId) - notInMailboxIds := q[QueryParamNotInMailboxId] - text := q.Get(QueryParamSearchText) - from := q.Get(QueryParamSearchFrom) - to := q.Get(QueryParamSearchTo) - cc := q.Get(QueryParamSearchCc) - bcc := q.Get(QueryParamSearchBcc) - subject := q.Get(QueryParamSearchSubject) - body := q.Get(QueryParamSearchBody) - - l := req.logger.With() - - offset, ok, err := req.parseNumericParam(QueryParamOffset, 0) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Int(QueryParamOffset, offset) - } - - limit, ok, err := req.parseNumericParam(QueryParamLimit, g.defaultEmailLimit) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Int(QueryParamLimit, limit) - } - - before, ok, err := req.parseDateParam(QueryParamSearchBefore) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Time(QueryParamSearchBefore, before) - } - - after, ok, err := req.parseDateParam(QueryParamSearchAfter) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Time(QueryParamSearchAfter, after) - } - - if mailboxId != "" { - l = l.Str(QueryParamMailboxId, logstr(mailboxId)) - } - if len(notInMailboxIds) > 0 { - l = l.Array(QueryParamNotInMailboxId, logstrarray(notInMailboxIds)) - } - if text != "" { - l = l.Str(QueryParamSearchText, logstr(text)) - } - if from != "" { - l = l.Str(QueryParamSearchFrom, logstr(from)) - } - if to != "" { - l = l.Str(QueryParamSearchTo, logstr(to)) - } - if cc != "" { - l = l.Str(QueryParamSearchCc, logstr(cc)) - } - if bcc != "" { - l = l.Str(QueryParamSearchBcc, logstr(bcc)) - } - if subject != "" { - l = l.Str(QueryParamSearchSubject, logstr(subject)) - } - if body != "" { - l = l.Str(QueryParamSearchBody, logstr(body)) - } - - minSize, ok, err := req.parseNumericParam(QueryParamSearchMinSize, 0) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Int(QueryParamSearchMinSize, minSize) - } - - maxSize, ok, err := req.parseNumericParam(QueryParamSearchMaxSize, 0) - if err != nil { - return errorResponse(err) - } - if ok { - l = l.Int(QueryParamSearchMaxSize, maxSize) - } - - logger := &log.Logger{Logger: l.Logger()} - - filter := jmap.EmailFilterCondition{ - Text: text, - InMailbox: mailboxId, - InMailboxOtherThan: notInMailboxIds, - From: from, - To: to, - Cc: cc, - Bcc: bcc, - Subject: subject, - Body: body, - Before: before, - After: after, - MinSize: minSize, - MaxSize: maxSize, - //HasKeyword: "", - } - - emails, jerr := g.jmap.QueryEmails(req.GetAccountId(), &filter, req.session, req.ctx, logger, offset, limit, false, 0) - if jerr != nil { - return req.errorResponseFromJmap(jerr) - } - - return etagResponse(emails, emails.SessionState, emails.QueryState) - }) + g.searchMessages(w, r) } } @@ -318,11 +434,13 @@ func (g Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) { } if result.Updated == nil { - // TODO(pbleser-oc) handle missing update response + return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", + "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) } updatedEmail, ok := result.Updated[messageId] if !ok { - // TODO(pbleser-oc) handle missing update response + return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", + "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } return response(updatedEmail, result.State) diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go index 9b33410573..15bc54ddf8 100644 --- a/services/groupware/pkg/groupware/groupware_error.go +++ b/services/groupware/pkg/groupware/groupware_error.go @@ -161,6 +161,7 @@ const ( ErrorCodeInvalidResponsePayload = "INVRSP" ErrorCodeInvalidRequestParameter = "INVPAR" ErrorCodeNonExistingAccount = "INVACC" + ErrorCodeApiInconsistency = "APIINC" ) var ( @@ -266,6 +267,12 @@ var ( Title: "Invalid Account Parameter", Detail: "The account the request is for does not exist.", } + ErrorApiInconsistency = GroupwareError{ + Status: http.StatusInternalServerError, + Code: ErrorCodeApiInconsistency, + Title: "API Inconsistency", + Detail: "Internal APIs returned unexpected data.", + } ) type ErrorOpt interface { diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index 32648f1e34..2132043671 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -309,6 +309,29 @@ func (r Request) parseDateParam(param string) (time.Time, bool, *Error) { return t, true, nil } +func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) { + q := r.r.URL.Query() + if !q.Has(param) { + return defaultValue, false, nil + } + + str := q.Get(param) + if str == "" { + return defaultValue, false, nil + } + + 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()) + return defaultValue, true, apiError(errorId, ErrorInvalidRequestParameter, + withDetail(msg), + withSource(&ErrorSource{Parameter: param}), + ) + } + return b, true, nil +} + func (r Request) body(target any) *Error { body := r.r.Body defer func(b io.ReadCloser) { @@ -439,7 +462,6 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func( g.log(response.err) w.Header().Add("Content-Type", ContentTypeJsonApi) render.Status(r, response.err.NumStatus) - w.WriteHeader(response.err.NumStatus) render.Render(w, r, errorResponses(*response.err)) return } @@ -456,14 +478,12 @@ func (g Groupware) respond(w http.ResponseWriter, r *http.Request, handler func( switch response.body { case nil: - render.Status(r, http.StatusNotFound) w.WriteHeader(http.StatusNotFound) case "": - render.Status(r, http.StatusNoContent) w.WriteHeader(http.StatusNoContent) default: render.Status(r, http.StatusOK) - render.JSON(w, r, response) + render.JSON(w, r, response.body) } } diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 4fc27267c7..687b6eccee 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -5,29 +5,33 @@ import ( ) const ( - UriParamAccount = "accountid" - UriParamMailboxId = "mailbox" - UriParamMessageId = "messageid" - UriParamBlobId = "blobid" - UriParamBlobName = "blobname" - QueryParamBlobType = "type" - QueryParamSince = "since" - 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" - QueryParamOffset = "offset" - QueryParamLimit = "limit" - HeaderSince = "if-none-match" + 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" ) func (g Groupware) Route(r chi.Router) { @@ -43,7 +47,7 @@ func (g Groupware) Route(r chi.Router) { r.Get("/{mailbox}/messages", g.GetAllMessages) }) r.Route("/messages", func(r chi.Router) { - r.Get("/", g.GetMessages) + r.Get("/", g.GetMessages) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&... r.Post("/", g.CreateMessage) r.Get("/{messageid}", g.GetMessagesById) r.Patch("/{messageid}", g.UpdateMessage) // or PUT?