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