mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-11 22:49:21 -06:00
groupware: add searching emails by their Message-Id + retrieving an email by its ID as message/rfc822
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,14 +12,22 @@ func TestSanitizeEmail(t *testing.T) {
|
||||
Subject: "test",
|
||||
BodyValues: map[string]jmap.EmailBodyValue{
|
||||
"koze92I1": {
|
||||
Value: `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
|
||||
Value: `<a onblur="alert(secret)" href="http://www.cyberdyne.com">Cyberdyne</a>`,
|
||||
},
|
||||
"zee7urae": {
|
||||
Value: `Hello. <a onblur="hack()" href="file:///download.exe">Click here</a> 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(`<a href="http://www.google.com" rel="nofollow">Google</a>`, safe.BodyValues["koze92I1"].Value)
|
||||
require.Equal(57, safe.HtmlBody[0].Size)
|
||||
require.Equal(`<a href="http://www.cyberdyne.com" rel="nofollow">Cyberdyne</a>`, 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user