From 5ae69edded13270cd0190221cf5e9d76f9db3b50 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 22 Oct 2025 12:15:24 +0200
Subject: [PATCH] groupware: add threadSize in email-by-id response
---
pkg/jmap/jmap_api_email.go | 167 ++++++++----------
pkg/jmap/jmap_model.go | 14 +-
pkg/jmap/jmap_tools.go | 18 --
pkg/jmap/jmap_tools_test.go | 47 -----
.../pkg/groupware/groupware_api_emails.go | 76 +++++---
.../pkg/groupware/groupware_request.go | 1 +
.../pkg/groupware/groupware_route.go | 2 +-
7 files changed, 131 insertions(+), 194 deletions(-)
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index f14b6f8fa..2799c3187 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -32,7 +32,7 @@ type Emails struct {
}
// Retrieve specific Emails by their id.
-func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool) (Emails, SessionState, Language, Error) {
+func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) (Emails, SessionState, Language, Error) {
logger = j.logger("GetEmails", session, logger)
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
@@ -50,6 +50,17 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
mark := EmailSetCommand{AccountId: accountId, Update: updates}
methodCalls = []Invocation{invocation(CommandEmailSet, mark, "0"), invokeGet}
}
+ if withThreads {
+ threads := ThreadGetRefCommand{
+ AccountId: accountId,
+ IdsRef: &ResultReference{
+ ResultOf: "1",
+ Name: CommandEmailGet,
+ Path: "/list/*/" + EmailPropertyThreadId,
+ },
+ }
+ methodCalls = append(methodCalls, invocation(CommandThreadGet, threads, "2"))
+ }
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
@@ -73,6 +84,14 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
if err != nil {
return Emails{}, err
}
+ if withThreads {
+ var threads ThreadGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, "2", &threads)
+ if err != nil {
+ return Emails{}, err
+ }
+ setThreadSize(&threads, response.List)
+ }
return Emails{Emails: response.List, State: response.State}, nil
})
}
@@ -636,14 +655,19 @@ type CreatedEmail struct {
State State `json:"state"`
}
-func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (CreatedEmail, SessionState, Language, Error) {
+func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (CreatedEmail, SessionState, Language, Error) {
+ set := EmailSetCommand{
+ AccountId: accountId,
+ Create: map[string]EmailCreate{
+ "c": email,
+ },
+ }
+ if replaceId != "" {
+ set.Destroy = []string{replaceId}
+ }
+
cmd, err := j.request(session, logger,
- invocation(CommandEmailSubmissionSet, EmailSetCommand{
- AccountId: accountId,
- Create: map[string]EmailCreate{
- "c": email,
- },
- }, "0"),
+ invocation(CommandEmailSet, set, "0"),
)
if err != nil {
return CreatedEmail{}, "", "", err
@@ -899,16 +923,6 @@ type EmailsSummary struct {
State State `json:"state"`
}
-type EmailWithThread struct {
- Email
- ThreadSize int `json:"threadSize,omitzero"`
-}
-
-type EmailsWithThreadSummary struct {
- Emails []EmailWithThread `json:"emails"`
- State State `json:"state"`
-}
-
var EmailSummaryProperties = []string{
EmailPropertyId,
EmailPropertyThreadId,
@@ -928,21 +942,26 @@ var EmailSummaryProperties = []string{
EmailPropertyPreview,
}
-func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint) (map[string]EmailsSummary, SessionState, Language, Error) {
+func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint, withThreads bool) (map[string]EmailsSummary, SessionState, Language, Error) {
logger = j.logger("QueryEmailSummaries", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
- invocations := make([]Invocation, len(uniqueAccountIds)*2)
+ factor := 2
+ if withThreads {
+ factor++
+ }
+
+ invocations := make([]Invocation, len(uniqueAccountIds)*factor)
for i, accountId := range uniqueAccountIds {
- invocations[i*2+0] = invocation(CommandEmailQuery, EmailQueryCommand{
+ invocations[i*factor+0] = invocation(CommandEmailQuery, EmailQueryCommand{
AccountId: accountId,
Filter: filter,
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
Limit: limit,
//CalculateTotal: false,
}, mcid(accountId, "0"))
- invocations[i*2+1] = invocation(CommandEmailGet, EmailGetRefCommand{
+ invocations[i*factor+1] = invocation(CommandEmailGet, EmailGetRefCommand{
AccountId: accountId,
IdsRef: &ResultReference{
Name: CommandEmailQuery,
@@ -951,6 +970,16 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
},
Properties: EmailSummaryProperties,
}, mcid(accountId, "1"))
+ if withThreads {
+ invocations[i*factor+2] = invocation(CommandThreadGet, ThreadGetRefCommand{
+ AccountId: accountId,
+ IdsRef: &ResultReference{
+ Name: CommandEmailGet,
+ Path: "/list/*/" + EmailPropertyThreadId,
+ ResultOf: mcid(accountId, "1"),
+ },
+ }, mcid(accountId, "2"))
+ }
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
@@ -968,87 +997,31 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
if len(response.NotFound) > 0 {
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
}
+ if withThreads {
+ var thread ThreadGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, mcid(accountId, "2"), &thread)
+ if err != nil {
+ return nil, err
+ }
+ setThreadSize(&thread, response.List)
+ }
+
resp[accountId] = EmailsSummary{Emails: response.List, State: response.State}
}
return resp, nil
})
}
-func (j *Client) QueryEmailSummariesWithThreadCount(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint) (map[string]EmailsWithThreadSummary, SessionState, Language, Error) {
- logger = j.logger("QueryEmailSummariesWithThreadCount", session, logger)
-
- uniqueAccountIds := structs.Uniq(accountIds)
-
- invocations := make([]Invocation, len(uniqueAccountIds)*3)
- for i, accountId := range uniqueAccountIds {
- invocations[i*3+0] = invocation(CommandEmailQuery, EmailQueryCommand{
- AccountId: accountId,
- Filter: filter,
- Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
- Limit: limit,
- //CalculateTotal: false,
- }, mcid(accountId, "0"))
- invocations[i*3+1] = invocation(CommandEmailGet, EmailGetRefCommand{
- AccountId: accountId,
- IdsRef: &ResultReference{
- Name: CommandEmailQuery,
- Path: "/ids/*",
- ResultOf: mcid(accountId, "0"),
- },
- Properties: EmailSummaryProperties,
- }, mcid(accountId, "1"))
- invocations[i*3+2] = invocation(CommandThreadGet, ThreadGetRefCommand{
- AccountId: accountId,
- IdsRef: &ResultReference{
- Name: CommandEmailGet,
- Path: "/list/*/" + EmailPropertyThreadId,
- ResultOf: mcid(accountId, "1"),
- },
- }, mcid(accountId, "2"))
+func setThreadSize(threads *ThreadGetResponse, emails []Email) {
+ threadSizeById := make(map[string]int, len(threads.List))
+ for _, thread := range threads.List {
+ threadSizeById[thread.Id] = len(thread.EmailIds)
}
- cmd, err := j.request(session, logger, invocations...)
- if err != nil {
- return nil, "", "", err
- }
-
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailsWithThreadSummary, Error) {
- resp := map[string]EmailsWithThreadSummary{}
- for _, accountId := range uniqueAccountIds {
- var response EmailGetResponse
- err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &response)
- if err != nil {
- return nil, err
- }
-
- var thread ThreadGetResponse
- err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, mcid(accountId, "2"), &thread)
- if err != nil {
- return nil, err
- }
-
- threadSizeById := make(map[string]int, len(thread.List))
- for _, thread := range thread.List {
- threadSizeById[thread.Id] = len(thread.EmailIds)
- }
-
- if len(response.NotFound) > 0 {
- // TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
- }
-
- list := make([]EmailWithThread, len(response.List))
- for i, email := range response.List {
- ts, ok := threadSizeById[email.ThreadId]
- if !ok {
- ts = 1
- }
- list[i] = EmailWithThread{
- Email: email,
- ThreadSize: ts,
- }
- }
-
- resp[accountId] = EmailsWithThreadSummary{Emails: list, State: response.State}
+ for i := range len(emails) {
+ ts, ok := threadSizeById[emails[i].ThreadId]
+ if !ok {
+ ts = 1
}
- return resp, nil
- })
+ emails[i].ThreadSize = ts
+ }
}
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 6ce4b8d75..389617fd6 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -2065,6 +2065,10 @@ type Email struct {
// example: $threadId
ThreadId string `json:"threadId,omitempty"`
+ // The number of emails (this one included) that are in the thread this email is in.
+ // Note that this is not part of the JMAP specification, and is only calculated when requested.
+ ThreadSize int `json:"threadSize,omitzero"`
+
// The set of Mailbox ids this Email belongs to.
//
// An Email in the mail store MUST belong to one or more Mailboxes at all times (until it is destroyed).
@@ -2179,7 +2183,7 @@ type Email struct {
// This is the full MIME structure of the message body, without recursing into message/rfc822 or message/global parts.
//
// Note that EmailBodyParts may have subParts if they are of type multipart/*.
- BodyStructure EmailBodyPart `json:"bodyStructure,omitzero"`
+ BodyStructure *EmailBodyPart `json:"bodyStructure,omitzero"`
// This is a map of partId to an EmailBodyValue object for none, some, or all text/* parts.
//
@@ -2812,12 +2816,6 @@ type MailboxQueryResponse struct {
Limit int `json:"limit,omitzero"`
}
-type EmailBodyStructure struct {
- Type string `json:"type"`
- PartId string `json:"partId"`
- Other map[string]any `mapstructure:",remain"`
-}
-
type EmailCreate struct {
// The set of Mailbox ids this Email belongs to.
//
@@ -2866,7 +2864,7 @@ type EmailCreate struct {
// This is the full MIME structure of the message body, without recursing into message/rfc822 or message/global parts.
//
// Note that EmailBodyParts may have subParts if they are of type multipart/*.
- BodyStructure EmailBodyStructure `json:"bodyStructure"`
+ BodyStructure *EmailBodyPart `json:"bodyStructure,omitempty"`
// This is a map of partId to an EmailBodyValue object for none, some, or all text/* parts.
BodyValues map[string]EmailBodyValue `json:"bodyValues,omitempty"`
diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go
index 6f2337eab..cb823cd7a 100644
--- a/pkg/jmap/jmap_tools.go
+++ b/pkg/jmap/jmap_tools.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "maps"
"reflect"
"strings"
"sync"
@@ -207,23 +206,6 @@ func retrieveResponseMatchParameters[T any](logger *log.Logger, data *Response,
return nil
}
-func (e EmailBodyStructure) MarshalJSON() ([]byte, error) {
- m := map[string]any{}
- maps.Copy(m, e.Other) // do this first to avoid overwriting type and partId
- m["type"] = e.Type
- m["partId"] = e.PartId
- return json.Marshal(m)
-}
-
-func (e *EmailBodyStructure) UnmarshalJSON(bs []byte) error {
- m := map[string]any{}
- err := json.Unmarshal(bs, &m)
- if err != nil {
- return err
- }
- return decodeMap(m, e)
-}
-
func (i *Invocation) MarshalJSON() ([]byte, error) {
// JMAP requests have a slightly unusual structure since they are not a JSON object
// but, instead, a three-element array composed of
diff --git a/pkg/jmap/jmap_tools_test.go b/pkg/jmap/jmap_tools_test.go
index 16a1b0b01..621331e54 100644
--- a/pkg/jmap/jmap_tools_test.go
+++ b/pkg/jmap/jmap_tools_test.go
@@ -88,53 +88,6 @@ func TestDeserializeEmailGetResponse(t *testing.T) {
require.Equal("cbejozsk1fgcviw7thwzsvtgmf1ep0a3izjoimj02jmtsunpeuwmsaya1yma", email.BlobId)
}
-func TestUnmarshallingUnknown(t *testing.T) {
- require := require.New(t)
-
- const text = `{
- "subject": "aaa",
- "bodyStructure": {
- "type": "a",
- "partId": "b",
- "header:x": "yz",
- "header:a": "bc"
- }
- }`
-
- var target EmailCreate
- err := json.Unmarshal([]byte(text), &target)
-
- require.NoError(err)
- require.Equal("aaa", target.Subject)
- bs := target.BodyStructure
- require.Equal("a", bs.Type)
- require.Equal("b", bs.PartId)
- require.Contains(bs.Other, "header:x")
- require.Equal(bs.Other["header:x"], "yz")
- require.Contains(bs.Other, "header:a")
- require.Equal(bs.Other["header:a"], "bc")
-}
-
-func TestMarshallingUnknown(t *testing.T) {
- require := require.New(t)
-
- source := EmailCreate{
- Subject: "aaa",
- BodyStructure: EmailBodyStructure{
- Type: "a",
- PartId: "b",
- Other: map[string]any{
- "header:x": "yz",
- "header:a": "bc",
- },
- },
- }
-
- result, err := json.Marshal(source)
- require.NoError(err)
- require.Equal(`{"subject":"aaa","bodyStructure":{"header:a":"bc","header:x":"yz","partId":"b","type":"a"}}`, string(result))
-}
-
func TestUnmarshallingError(t *testing.T) {
require := require.New(t)
diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go
index 855a99253..312b3cce7 100644
--- a/services/groupware/pkg/groupware/groupware_api_emails.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -204,7 +204,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
if len(ids) == 1 {
logger := log.From(l.Str("id", log.SafeString(id)))
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen)
+ emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, true)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -220,7 +220,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
} else {
logger := log.From(l.Array("ids", log.SafeStringArray(ids)))
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen)
+ emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, false)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -270,7 +270,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
logger := log.From(l)
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false)
+ emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -298,7 +298,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
l = contextAppender(l)
logger := log.From(l)
- emails, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false)
+ emails, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
return req.apiErrorFromJmap(req.observeJmapError(jerr))
}
@@ -869,6 +869,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
})
}
+/*
type EmailCreation struct {
MailboxIds []string `json:"mailboxIds,omitempty"`
Keywords []string `json:"keywords,omitempty"`
@@ -879,6 +880,7 @@ type EmailCreation struct {
BodyStructure jmap.EmailBodyStructure `json:"bodyStructure"`
BodyValues map[string]jmap.EmailBodyValue `json:"bodyValues,omitempty"`
}
+*/
func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
@@ -890,25 +892,15 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- var body EmailCreation
+ var body jmap.Email
err := req.body(&body)
if err != nil {
return errorResponse(err)
}
- mailboxIdsMap := map[string]bool{}
- for _, mailboxId := range body.MailboxIds {
- mailboxIdsMap[mailboxId] = true
- }
-
- keywordsMap := map[string]bool{}
- for _, keyword := range body.Keywords {
- keywordsMap[keyword] = true
- }
-
create := jmap.EmailCreate{
- MailboxIds: mailboxIdsMap,
- Keywords: keywordsMap,
+ MailboxIds: body.MailboxIds,
+ Keywords: body.Keywords,
From: body.From,
Subject: body.Subject,
ReceivedAt: body.ReceivedAt,
@@ -917,7 +909,46 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
BodyValues: body.BodyValues,
}
- created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, req.session, req.ctx, logger, req.language())
+ created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, "", req.session, req.ctx, logger, req.language())
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return response(created.Email, sessionState, lang)
+ })
+}
+
+func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ logger := req.logger
+
+ accountId, gwerr := req.GetAccountIdForMail()
+ if gwerr != nil {
+ return errorResponse(gwerr)
+ }
+
+ replaceId := chi.URLParam(r, UriParamEmailId)
+
+ logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
+
+ var body jmap.Email
+ err := req.body(&body)
+ if err != nil {
+ return errorResponse(err)
+ }
+
+ create := jmap.EmailCreate{
+ MailboxIds: body.MailboxIds,
+ Keywords: body.Keywords,
+ From: body.From,
+ Subject: body.Subject,
+ ReceivedAt: body.ReceivedAt,
+ SentAt: body.SentAt,
+ BodyStructure: body.BodyStructure,
+ BodyValues: body.BodyValues,
+ }
+
+ created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, replaceId, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -1361,7 +1392,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
reqId := req.GetRequestId()
getEmailsBefore := time.Now()
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes, false)
+ emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes, false, false)
getEmailsDuration := time.Since(getEmailsBefore)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
@@ -1610,7 +1641,7 @@ type EmailSummary struct {
Preview string `json:"preview,omitempty"`
}
-func summarizeEmail(accountId string, email jmap.EmailWithThread) EmailSummary {
+func summarizeEmail(accountId string, email jmap.Email) EmailSummary {
return EmailSummary{
AccountId: accountId,
Id: email.Id,
@@ -1635,7 +1666,7 @@ func summarizeEmail(accountId string, email jmap.EmailWithThread) EmailSummary {
type emailWithAccountId struct {
accountId string
- email jmap.EmailWithThread
+ email jmap.Email
}
// When the request succeeds.
@@ -1728,8 +1759,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
logger := log.From(l)
- // emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit)
- emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummariesWithThreadCount(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit)
+ emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit, true)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index 56d6b6739..680fd4932 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -290,6 +290,7 @@ func (r Request) body(target any) *Error {
err := json.NewDecoder(body).Decode(target)
if err != nil {
+ r.logger.Warn().Msgf("failed to deserialize the request body: %s", err.Error())
return r.observedParameterError(ErrorInvalidRequestBody, withSource(&ErrorSource{Pointer: "/"})) // we don't get any details here
}
return nil
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 0c469d4db..e9a0bb802 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -97,7 +97,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Post("/", g.CreateEmail)
r.Delete("/", g.DeleteEmails)
r.Get("/{emailid}", g.GetEmailsById) // Accept:message/rfc822
- // r.Put("/{emailid}", g.ReplaceEmail) // TODO
+ r.Put("/{emailid}", g.ReplaceEmail)
r.Patch("/{emailid}", g.UpdateEmail)
r.Patch("/{emailid}/keywords", g.UpdateEmailKeywords)
r.Post("/{emailid}/keywords", g.AddEmailKeywords)