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?