mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-13 20:07:41 -04:00
groupware: fix email search, add variant that includes the full emails
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user