From 641d203efe8733a23576a647292e7051389fac80 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Wed, 10 Sep 2025 11:49:00 +0200 Subject: [PATCH] refactor(groupware): rename "Messages" to "Email" everywhere There was really no reason to go with "Messages" as far as the vocabulary of the Groupware API goes, since the objects those APIs serve are "Emails", to stick with the wording of the JMAP specification. --- services/groupware/api-params.yaml | 2 + services/groupware/apidoc.yml | 6 +- .../pkg/groupware/groupware_api_messages.go | 112 ++++++++++-------- .../pkg/groupware/groupware_route.go | 20 ++-- 4 files changed, 78 insertions(+), 62 deletions(-) diff --git a/services/groupware/api-params.yaml b/services/groupware/api-params.yaml index 8fd7cf8810..15fad6e772 100644 --- a/services/groupware/api-params.yaml +++ b/services/groupware/api-params.yaml @@ -3,3 +3,5 @@ params: description: The identifier of the Account to use for this operation mailbox: description: The identifier of the Mailbox to perform this operation on + emailid: + description: The identifier of the Email to perform this operation on diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml index 42be3e0364..069842ac6a 100644 --- a/services/groupware/apidoc.yml +++ b/services/groupware/apidoc.yml @@ -14,8 +14,8 @@ tags: - name: mailbox x-displayName: Mailboxes description: APIs that pertain to mailboxes - - name: message - x-displayName: Messages + - name: email + x-displayName: Emails description: APIs about emails - name: vacation x-displayName: Vacation Responses @@ -31,7 +31,7 @@ x-tagGroups: tags: - identity - mailbox - - message + - email - vacation components: securitySchemes: diff --git a/services/groupware/pkg/groupware/groupware_api_messages.go b/services/groupware/pkg/groupware/groupware_api_messages.go index b3490b9cc2..292ee50ea0 100644 --- a/services/groupware/pkg/groupware/groupware_api_messages.go +++ b/services/groupware/pkg/groupware/groupware_api_messages.go @@ -15,8 +15,8 @@ import ( ) // When the request succeeds without a "since" query parameter. -// swagger:response GetAllMessagesInMailbox200 -type SwaggerGetAllMessagesInMailbox200 struct { +// swagger:response GetAllEmailsInMailbox200 +type SwaggerGetAllEmailsInMailbox200 struct { // in: body Body struct { *jmap.Emails @@ -24,15 +24,15 @@ type SwaggerGetAllMessagesInMailbox200 struct { } // When the request succeeds with a "since" query parameter. -// swagger:response GetAllMessagesInMailboxSince200 -type SwaggerGetAllMessagesInMailboxSince200 struct { +// swagger:response GetAllEmailsInMailboxSince200 +type SwaggerGetAllEmailsInMailboxSince200 struct { // in: body Body struct { *jmap.MailboxChanges } } -// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/messages message get_all_messages_in_mailbox +// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails email get_all_emails_in_mailbox // Get all the emails in a mailbox. // // Retrieve the list of all the emails that are in a given mailbox. @@ -48,12 +48,12 @@ type SwaggerGetAllMessagesInMailboxSince200 struct { // // responses: // -// 200: GetAllMessagesInMailbox200 -// 200: GetAllMessagesInMailboxSince200 +// 200: GetAllEmailsInMailbox200 +// 200: GetAllEmailsInMailboxSince200 // 400: ErrorResponse400 // 404: ErrorResponse404 // 500: ErrorResponse500 -func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) { mailboxId := chi.URLParam(r, UriParamMailboxId) since := r.Header.Get(HeaderSince) @@ -119,12 +119,12 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque } } -func (g *Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, UriParamMessageId) +func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, UriParamEmailId) g.respond(w, r, func(req Request) Response { ids := strings.Split(id, ",") if len(ids) < 1 { - return req.parameterErrorResponse(UriParamMessageId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamMessageId, log.SafeString(id), "empty list of mail ids")) + return req.parameterErrorResponse(UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids")) } accountId, err := req.GetAccountIdForMail() @@ -132,18 +132,34 @@ func (g *Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) { return errorResponse(err) } - logger := log.From(req.logger.With().Str("id", log.SafeString(id)).Str(logAccountId, log.SafeString(accountId))) - - emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes) - if jerr != nil { - return req.errorResponseFromJmap(jerr) + l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) + if len(ids) == 1 { + logger := log.From(l.Str("id", log.SafeString(id))) + emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + if len(emails.Emails) < 1 { + return notFoundResponse(sessionState) + } else { + return etagResponse(emails.Emails[0], sessionState, emails.State) + } + } else { + logger := log.From(l.Array("ids", log.SafeStringArray(ids))) + emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + if len(emails.Emails) < 1 { + return notFoundResponse(sessionState) + } else { + return etagResponse(emails.Emails, sessionState, emails.State) + } } - - return etagResponse(emails, sessionState, emails.State) }) } -func (g *Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, since string) { +func (g *Groupware) getEmailsSince(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.parseUIntParam(QueryParamMaxChanges, 0) @@ -171,7 +187,7 @@ func (g *Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, sin }) } -type MessageSearchSnippetsResults struct { +type EmailSearchSnippetsResults struct { Results []jmap.SearchSnippet `json:"results,omitempty"` Total uint `json:"total,omitzero"` Limit uint `json:"limit,omitzero"` @@ -188,7 +204,7 @@ type SnippetWithoutEmailId struct { Preview string `json:"preview,omitempty"` } -type MessageSearchResults struct { +type EmailSearchResults struct { Results []EmailWithSnippets `json:"results"` Total uint `json:"total,omitzero"` Limit uint `json:"limit,omitzero"` @@ -325,7 +341,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin return true, filter, offset, limit, logger, nil } -func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { ok, filter, offset, limit, logger, err := g.buildFilter(req) if !ok { @@ -379,7 +395,7 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { } } - return etagResponse(MessageSearchResults{ + return etagResponse(EmailSearchResults{ Results: flattened, Total: results.Total, Limit: results.Limit, @@ -397,7 +413,7 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { return req.errorResponseFromJmap(jerr) } - return etagResponse(MessageSearchSnippetsResults{ + return etagResponse(EmailSearchSnippetsResults{ Results: results.Snippets, Total: results.Total, Limit: results.Limit, @@ -407,22 +423,22 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { }) } -func (g *Groupware) GetMessages(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() since := q.Get(QueryParamSince) if since == "" { since = r.Header.Get(HeaderSince) } if since != "" { - // get messages changes since a given state - g.getMessagesSince(w, r, since) + // get email changes since a given state + g.getEmailsSince(w, r, since) } else { // do a search - g.searchMessages(w, r) + g.searchEmails(w, r) } } -type MessageCreation struct { +type EmailCreation struct { MailboxIds []string `json:"mailboxIds,omitempty"` Keywords []string `json:"keywords,omitempty"` From []jmap.EmailAddress `json:"from,omitempty"` @@ -433,7 +449,7 @@ type MessageCreation struct { BodyValues map[string]jmap.EmailBodyValue `json:"bodyValues,omitempty"` } -func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { logger := req.logger @@ -443,7 +459,7 @@ func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) { } logger = log.From(logger.With().Str(logAccountId, accountId)) - var body MessageCreation + var body EmailCreation err := req.body(&body) if err != nil { return errorResponse(err) @@ -479,12 +495,12 @@ func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) { }) } -func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - messageId := chi.URLParam(r, UriParamMessageId) + emailId := chi.URLParam(r, UriParamEmailId) l := req.logger.With() - l.Str(UriParamMessageId, messageId) + l.Str(UriParamEmailId, emailId) accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { @@ -501,7 +517,7 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) { } updates := map[string]jmap.EmailUpdate{ - messageId: body, + emailId: body, } result, sessionState, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger) @@ -513,7 +529,7 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) { 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] + updatedEmail, ok := result.Updated[emailId] if !ok { 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"))) @@ -524,12 +540,12 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) { } -func (g *Groupware) DeleteMessage(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - messageId := chi.URLParam(r, UriParamMessageId) + emailId := chi.URLParam(r, UriParamEmailId) l := req.logger.With() - l.Str(UriParamMessageId, messageId) + l.Str(UriParamEmailId, emailId) accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { @@ -539,7 +555,7 @@ func (g *Groupware) DeleteMessage(w http.ResponseWriter, r *http.Request) { logger := log.From(l) - _, sessionState, jerr := g.jmap.DeleteEmails(accountId, []string{messageId}, req.session, req.ctx, logger) + _, sessionState, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -548,17 +564,15 @@ func (g *Groupware) DeleteMessage(w http.ResponseWriter, r *http.Request) { }) } -type AboutMessageEmailsEvent struct { +type AboutEmailsEvent struct { Id string `json:"id"` Source string `json:"source"` Emails []jmap.Email `json:"emails"` } -type AboutMessageResponse struct { +type AboutEmailResponse struct { Email jmap.Email `json:"email"` RequestId string `json:"requestId"` - // IV - // Key (AES-256) } func relatedEmails(email jmap.Email, beacon time.Time, days uint) jmap.EmailFilterElement { @@ -598,8 +612,8 @@ func relatedEmails(email jmap.Email, beacon time.Time, days uint) jmap.EmailFilt return filter } -func (g *Groupware) RelatedToMessage(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, UriParamMessageId) +func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, UriParamEmailId) g.respond(w, r, func(req Request) Response { l := req.logger.With().Str(logEmailId, log.SafeString(id)) @@ -665,7 +679,7 @@ func (g *Groupware) RelatedToMessage(w http.ResponseWriter, r *http.Request) { related := filterEmails(results.Emails, email) l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameSender, len(related)) if len(related) > 0 { - req.push(RelationEntityEmail, AboutMessageEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender}) + req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender}) } } }) @@ -682,12 +696,12 @@ func (g *Groupware) RelatedToMessage(w http.ResponseWriter, r *http.Request) { related := filterEmails(emails, email) l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameThread, len(related)) if len(related) > 0 { - req.push(RelationEntityEmail, AboutMessageEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameThread}) + req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameThread}) } } }) - return etagResponse(AboutMessageResponse{ + return etagResponse(AboutEmailResponse{ Email: email, RequestId: reqId, }, sessionState, emails.State) diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 895ac58f02..664fb61e99 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -11,7 +11,7 @@ const ( UriParamAccountId = "accountid" UriParamMailboxId = "mailbox" - UriParamMessageId = "messageid" + UriParamEmailId = "emailid" UriParamBlobId = "blobid" UriParamBlobName = "blobname" UriParamStreamId = "stream" @@ -61,17 +61,17 @@ func (g *Groupware) Route(r chi.Router) { r.Route("/mailboxes", func(r chi.Router) { r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed= r.Get("/{mailbox}", g.GetMailbox) - r.Get("/{mailbox}/messages", g.GetAllMessagesInMailbox) + r.Get("/{mailbox}/emails", g.GetAllEmailsInMailbox) r.Get("/{mailbox}/changes", g.GetMailboxChanges) }) - r.Route("/messages", func(r chi.Router) { - r.Get("/", g.GetMessages) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&... - r.Post("/", g.CreateMessage) - r.Get("/{messageid}", g.GetMessagesById) - // r.Put("/{messageid}", g.ReplaceMessage) // TODO - r.Patch("/{messageid}", g.UpdateMessage) - r.Delete("/{messageid}", g.DeleteMessage) - Report(r, "/{messageid}", g.RelatedToMessage) + r.Route("/emails", func(r chi.Router) { + r.Get("/", g.GetEmails) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&... + r.Post("/", g.CreateEmail) + r.Get("/{emailid}", g.GetEmailsById) + // r.Put("/{emailid}", g.ReplaceEmail) // TODO + r.Patch("/{emailid}", g.UpdateEmail) + r.Delete("/{emailid}", g.DeleteEmail) + Report(r, "/{emailid}", g.RelatedToEmail) }) r.Route("/blobs", func(r chi.Router) { r.Get("/{blobid}", g.GetBlob)