From fa6b695f241c786ff2e98a0a5470d44036ca7ae1 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 21 Oct 2025 10:16:50 +0200 Subject: [PATCH] groupware: add markAsSeen=true to mark an email as $seen before it is retrieved --- pkg/jmap/jmap_api_email.go | 31 ++++++++++++++++-- .../pkg/groupware/groupware_api_emails.go | 32 ++++++++++++++----- .../pkg/groupware/groupware_route.go | 1 + 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index c856f0b3e..3824fe5bb 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -32,22 +32,47 @@ 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) (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) (Emails, SessionState, Language, Error) { logger = j.logger("GetEmails", session, logger) get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies} if maxBodyValueBytes > 0 { get.MaxBodyValueBytes = maxBodyValueBytes } + invokeGet := invocation(CommandEmailGet, get, "1") - cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0")) + methodCalls := []Invocation{invokeGet} + if markAsSeen { + patch := map[string]bool{ + JmapKeywordSeen: true, + } + updates := make(map[string]EmailUpdate, len(ids)) + for _, id := range ids { + updates[id] = EmailUpdate{"keywords": patch} + } + mark := EmailSetCommand{AccountId: accountId, Update: updates} + methodCalls = []Invocation{invocation(CommandEmailSet, mark, "0"), invokeGet} + } + + cmd, err := j.request(session, logger, methodCalls...) if err != nil { logger.Error().Err(err).Send() return Emails{}, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) } return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, Error) { + if markAsSeen { + var markResponse EmailSetResponse + err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &markResponse) + if err != nil { + return Emails{}, err + } + for _, seterr := range markResponse.NotUpdated { + // TODO we don't have a way to compose multiple set errors yet + return Emails{}, setErrorError(seterr, EmailType) + } + } var response EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "0", &response) + err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &response) if err != nil { return Emails{}, err } diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go index 0e90b780a..b64fc1f18 100644 --- a/services/groupware/pkg/groupware/groupware_api_emails.go +++ b/services/groupware/pkg/groupware/groupware_api_emails.go @@ -149,6 +149,14 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { return err } + _, ok, err := req.parseBoolParam(QueryParamMarkAsSeen, false) + if err != nil { + return err + } + if ok { + return req.parameterError(QueryParamMarkAsSeen, fmt.Sprintf("when the Accept header is set to '%s', the API does not support setting %s", accept, QueryParamMarkAsSeen)) + } + logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)).Str("id", log.SafeString(id)).Str("accept", log.SafeString(accept))) blobId, _, _, jerr := g.jmap.GetEmailBlobId(accountId, req.session, req.ctx, logger, req.language(), id) @@ -178,11 +186,19 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { return errorResponse(err) } l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) - if len(ids) == 1 { - l = l.Str("id", log.SafeString(id)) - logger := log.From(l) - emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes) + markAsSeen, ok, err := req.parseBoolParam(QueryParamMarkAsSeen, false) + if err != nil { + return errorResponse(err) + } + if ok { + l = l.Bool(QueryParamMarkAsSeen, markAsSeen) + } + + 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) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -194,7 +210,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) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -240,7 +256,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) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -265,7 +281,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) + emails, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false) if jerr != nil { return req.apiErrorFromJmap(req.observeJmapError(jerr)) } @@ -1319,7 +1335,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) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes, false) getEmailsDuration := time.Since(getEmailsBefore) if jerr != nil { return req.errorResponseFromJmap(jerr) diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 01a185cd1..0c469d4db 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -53,6 +53,7 @@ const ( QueryParamAttachmentBlobId = "blobId" QueryParamSeen = "seen" QueryParamUndesirable = "undesirable" + QueryParamMarkAsSeen = "markAsSeen" HeaderSince = "if-none-match" )