diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index 3d62d48491..c856f0b3ed 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -55,6 +55,29 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
})
}
+func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (string, SessionState, Language, Error) {
+ logger = j.logger("GetEmailBlobId", session, logger)
+
+ get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}}
+ cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0"))
+ if err != nil {
+ logger.Error().Err(err).Send()
+ return "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ }
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (string, Error) {
+ var response EmailGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "0", &response)
+ if err != nil {
+ return "", err
+ }
+ if len(response.List) != 1 {
+ return "", nil
+ }
+ email := response.List[0]
+ return email.BlobId, nil
+ })
+}
+
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, offset uint, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Language, Error) {
logger = j.loggerParams("GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context {
diff --git a/services/groupware/pkg/groupware/groupware_api_blob.go b/services/groupware/pkg/groupware/groupware_api_blob.go
index e479303650..73ae4c198c 100644
--- a/services/groupware/pkg/groupware/groupware_api_blob.go
+++ b/services/groupware/pkg/groupware/groupware_api_blob.go
@@ -79,9 +79,6 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(req.r, UriParamBlobName)
q := req.r.URL.Query()
typ := q.Get(QueryParamBlobType)
- if typ == "" {
- typ = DefaultBlobDownloadType
- }
accountId, gwerr := req.GetAccountIdForBlob()
if gwerr != nil {
@@ -89,44 +86,51 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
- blob, lang, jerr := g.jmap.DownloadBlobStream(accountId, blobId, name, typ, req.session, req.ctx, logger, req.language())
- if blob != nil && blob.Body != nil {
- defer func(Body io.ReadCloser) {
- err := Body.Close()
- if err != nil {
- logger.Error().Err(err).Msg("failed to close response body")
- }
- }(blob.Body)
- }
- if jerr != nil {
- return req.apiErrorFromJmap(jerr)
- }
- if blob == nil {
- w.WriteHeader(http.StatusNotFound)
- return nil
- }
-
- if blob.Type != "" {
- w.Header().Add("Content-Type", blob.Type)
- }
- if blob.CacheControl != "" {
- w.Header().Add("Cache-Control", blob.CacheControl)
- }
- if blob.ContentDisposition != "" {
- w.Header().Add("Content-Disposition", blob.ContentDisposition)
- }
- if blob.Size >= 0 {
- w.Header().Add("Content-Size", strconv.Itoa(blob.Size))
- }
- if lang != "" {
- w.Header().Add("Content-Language", string(lang))
- }
-
- _, err := io.Copy(w, blob.Body)
- if err != nil {
- return req.observedParameterError(ErrorStreamingResponse)
- }
-
- return nil
+ return req.serveBlob(blobId, name, typ, logger, accountId, w)
})
}
+
+func (r *Request) serveBlob(blobId string, name string, typ string, logger *log.Logger, accountId string, w http.ResponseWriter) *Error {
+ if typ == "" {
+ typ = DefaultBlobDownloadType
+ }
+ blob, lang, jerr := r.g.jmap.DownloadBlobStream(accountId, blobId, name, typ, r.session, r.ctx, logger, r.language())
+ if blob != nil && blob.Body != nil {
+ defer func(Body io.ReadCloser) {
+ err := Body.Close()
+ if err != nil {
+ logger.Error().Err(err).Msg("failed to close response body")
+ }
+ }(blob.Body)
+ }
+ if jerr != nil {
+ return r.apiErrorFromJmap(jerr)
+ }
+ if blob == nil {
+ w.WriteHeader(http.StatusNotFound)
+ return nil
+ }
+
+ if blob.Type != "" {
+ w.Header().Add("Content-Type", blob.Type)
+ }
+ if blob.CacheControl != "" {
+ w.Header().Add("Cache-Control", blob.CacheControl)
+ }
+ if blob.ContentDisposition != "" {
+ w.Header().Add("Content-Disposition", blob.ContentDisposition)
+ }
+ if blob.Size >= 0 {
+ w.Header().Add("Content-Size", strconv.Itoa(blob.Size))
+ }
+ if lang != "" {
+ w.Header().Add("Content-Language", string(lang))
+ }
+
+ _, err := io.Copy(w, blob.Body)
+ if err != nil {
+ return r.observedParameterError(ErrorStreamingResponse)
+ }
+
+ return nil
+}
diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go
index 1ab735c2e9..0e90b780ae 100644
--- a/services/groupware/pkg/groupware/groupware_api_emails.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -135,42 +135,77 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
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(UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids"))
- }
+ ids := strings.Split(id, ",")
- accountId, err := req.GetAccountIdForMail()
- if err != nil {
- return errorResponse(err)
- }
+ accept := r.Header.Get("Accept")
+ if accept == "message/rfc822" {
+ g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
+ if len(ids) != 1 {
+ return req.parameterError(UriParamEmailId, fmt.Sprintf("when the Accept header is set to '%s', the API only supports serving a single email id", accept))
+ }
- l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
- 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)
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return err
+ }
+
+ 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)
if jerr != nil {
- return req.errorResponseFromJmap(jerr)
+ return req.apiErrorFromJmap(req.observeJmapError(jerr))
}
- if len(emails.Emails) < 1 {
- return notFoundResponse(sessionState)
+ if blobId == "" {
+ return nil
} else {
- return etagResponse(g.sanitizeEmail(emails.Emails[0]), sessionState, emails.State, lang)
+ name := blobId + ".eml"
+ typ := accept
+ accountId, gwerr := req.GetAccountIdForBlob()
+ if gwerr != nil {
+ return gwerr
+ }
+ return req.serveBlob(blobId, name, typ, logger, accountId, w)
}
- } 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)
- if jerr != nil {
- return req.errorResponseFromJmap(jerr)
+ })
+ } else {
+ g.respond(w, r, func(req Request) Response {
+ if len(ids) < 1 {
+ return req.parameterErrorResponse(UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids"))
}
- if len(emails.Emails) < 1 {
- return notFoundResponse(sessionState)
+
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ 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)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+ if len(emails.Emails) < 1 {
+ return notFoundResponse(sessionState)
+ } else {
+ return etagResponse(g.sanitizeEmail(emails.Emails[0]), sessionState, emails.State, lang)
+ }
} else {
- return etagResponse(g.sanitizeEmails(emails.Emails), sessionState, emails.State, lang)
+ 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)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+ if len(emails.Emails) < 1 {
+ return notFoundResponse(sessionState)
+ } else {
+ return etagResponse(g.sanitizeEmails(emails.Emails), sessionState, emails.State, lang)
+ }
}
- }
- })
+ })
+ }
}
func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) {
@@ -369,6 +404,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
subject := q.Get(QueryParamSearchSubject)
body := q.Get(QueryParamSearchBody)
keywords := q[QueryParamSearchKeyword]
+ messageId := q.Get(QueryParamSearchMessageId)
snippets := false
@@ -433,6 +469,9 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
if body != "" {
l = l.Str(QueryParamSearchBody, log.SafeString(body))
}
+ if messageId != "" {
+ l = l.Str(QueryParamSearchMessageId, log.SafeString(messageId))
+ }
minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0)
if err != nil {
@@ -468,6 +507,14 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
After: after,
MinSize: minSize,
MaxSize: maxSize,
+ Header: []string{},
+ }
+ if messageId != "" {
+ // The array MUST contain either one or two elements.
+ // The first element is the name of the header field to match against.
+ // The second (optional) element is the text to look for in the header field value.
+ // If not supplied, the message matches simply if it has a header field of the given name.
+ firstFilter.Header = []string{"Message-ID", messageId}
}
filter = &firstFilter
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 4c86ef394b..01a185cd12 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -42,6 +42,7 @@ const (
QueryParamSearchMinSize = "minsize"
QueryParamSearchMaxSize = "maxsize"
QueryParamSearchKeyword = "keyword"
+ QueryParamSearchMessageId = "messageId"
QueryParamSearchFetchBodies = "fetchbodies"
QueryParamSearchFetchEmails = "fetchemails"
QueryParamOffset = "offset"
@@ -94,7 +95,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetEmails) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&...
r.Post("/", g.CreateEmail)
r.Delete("/", g.DeleteEmails)
- r.Get("/{emailid}", g.GetEmailsById)
+ r.Get("/{emailid}", g.GetEmailsById) // Accept:message/rfc822
// r.Put("/{emailid}", g.ReplaceEmail) // TODO
r.Patch("/{emailid}", g.UpdateEmail)
r.Patch("/{emailid}/keywords", g.UpdateEmailKeywords)
@@ -102,6 +103,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Delete("/{emailid}/keywords", g.RemoveEmailKeywords)
r.Delete("/{emailid}", g.DeleteEmail)
Report(r, "/{emailid}", g.RelatedToEmail)
+ r.Get("/{emailid}/related", g.RelatedToEmail)
r.Get("/{emailid}/attachments", g.GetEmailAttachments) // ?partId=&name=?&blobId=?
})
r.Route("/blobs", func(r chi.Router) {
diff --git a/services/groupware/pkg/groupware/groupware_test.go b/services/groupware/pkg/groupware/groupware_test.go
index 1017a1af3f..dac9191877 100644
--- a/services/groupware/pkg/groupware/groupware_test.go
+++ b/services/groupware/pkg/groupware/groupware_test.go
@@ -12,14 +12,22 @@ func TestSanitizeEmail(t *testing.T) {
Subject: "test",
BodyValues: map[string]jmap.EmailBodyValue{
"koze92I1": {
- Value: `Google`,
+ Value: `Cyberdyne`,
+ },
+ "zee7urae": {
+ Value: `Hello. Click here for AI slop.`,
},
},
HtmlBody: []jmap.EmailBodyPart{
{
PartId: "koze92I1",
Type: "text/html",
- Size: 65,
+ Size: 71,
+ },
+ {
+ PartId: "zee7urae",
+ Type: "text/html",
+ Size: 81,
},
},
}
@@ -29,6 +37,8 @@ func TestSanitizeEmail(t *testing.T) {
safe := g.sanitizeEmail(email)
require := require.New(t)
- require.Equal(`Google`, safe.BodyValues["koze92I1"].Value)
- require.Equal(57, safe.HtmlBody[0].Size)
+ require.Equal(`Cyberdyne`, safe.BodyValues["koze92I1"].Value)
+ require.Equal(63, safe.HtmlBody[0].Size)
+ require.Equal(`Hello. Click here for AI slop.`, safe.BodyValues["zee7urae"].Value)
+ require.Equal(30, safe.HtmlBody[1].Size)
}