groupware: fix email search, add variant that includes the full emails

This commit is contained in:
Pascal Bleser
2025-08-07 13:53:59 +02:00
parent dd3c6a1f7f
commit f2106ec809
7 changed files with 498 additions and 163 deletions

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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?