API documentation changes for groupware-apidocs

* add example generator infrastructure, with some examples for pkg/jmap
   and pkg/groupware, with more needing to be done

 * alter the apidoc Makefile to stop using go-swagger but, instead, use
   the openapi.yml file that must be dropped into that directory using
   groupware-apidocs (will improve the integration there later)

 * add Makefile target to generate examples

 * bump redocly from 2.4.0 to 2.14.5

 * introduce Request.PathParam() and .PathParamDoc() to improve API
   documentation, as well as future-proofing

 * improve X-Request-ID and Trace-Id header handling in the middleware
   by logging it safely when an error occurs in the middleware
This commit is contained in:
Pascal Bleser
2026-01-22 09:26:19 +01:00
parent 7dd64cc3ea
commit 531e50fb73
31 changed files with 1701 additions and 780 deletions
@@ -0,0 +1 @@
/apidoc-examples.json
@@ -33,7 +33,8 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
if err != nil {
return errorResponse(single(accountId), err)
}
return etagResponse(single(accountId), account, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
var body jmap.Account = account
return etagResponse(single(accountId), body, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
})
}
@@ -66,7 +67,8 @@ func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) {
}
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithId) int { return strings.Compare(a.AccountId, b.AccountId) })
return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), list, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
var RBODY []AccountWithId = list
return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
})
}
@@ -94,7 +96,8 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt
}
// sort on accountId to have a stable order that remains the same with every query
slices.SortFunc(list, func(a, b AccountWithIdAndIdentities) int { return strings.Compare(a.AccountId, b.AccountId) })
return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), list, sessionState, AccountResponseObjectType, state, lang)
var RBODY []AccountWithIdAndIdentities = list
return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang)
})
}
@@ -108,34 +111,3 @@ type AccountWithIdAndIdentities struct {
jmap.Account
Identities []jmap.Identity `json:"identities,omitempty"`
}
type AccountBootstrapResponse struct {
// The API version.
Version string `json:"version"`
// A list of capabilities of this API version.
Capabilities []string `json:"capabilities"`
// API limits.
Limits IndexLimits `json:"limits"`
// Accounts that are available to the user.
//
// The key of the mapis the identifier.
Accounts map[string]IndexAccount `json:"accounts"`
// Primary accounts for usage types.
PrimaryAccounts IndexPrimaryAccounts `json:"primaryAccounts"`
// Mailboxes.
Mailboxes map[string][]jmap.Mailbox `json:"mailboxes"`
}
// When the request suceeds.
// swagger:response GetAccountBootstrapResponse200
type SwaggerAccountBootstrapResponse struct {
// in: body
Body struct {
*AccountBootstrapResponse
}
}
@@ -1,12 +1,10 @@
package groupware
import (
"fmt"
"io"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -22,9 +20,9 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(logAccountId, accountId)
blobId := chi.URLParam(req.r, UriParamBlobId)
if blobId == "" {
return req.parameterErrorResponse(single(accountId), UriParamBlobId, fmt.Sprintf("Invalid value for path parameter '%v': empty", UriParamBlobId))
blobId, err := req.PathParam(UriParamBlobId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamBlobId, blobId)
@@ -34,8 +32,7 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
blob := res
if blob == nil {
if res == nil {
return notFoundResponse(single(accountId), sessionState)
}
return etagResponse(single(accountId), res, sessionState, BlobResponseObjectType, state, lang)
@@ -72,10 +69,15 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
blobId := chi.URLParam(req.r, UriParamBlobId)
name := chi.URLParam(req.r, UriParamBlobName)
q := req.r.URL.Query()
typ := q.Get(QueryParamBlobType)
blobId, err := req.PathParam(UriParamBlobId)
if err != nil {
return err
}
name, err := req.PathParam(UriParamBlobName)
if err != nil {
return err
}
typ, _ := req.getStringParam(QueryParamBlobType, "")
accountId, gwerr := req.GetAccountIdForBlob()
if gwerr != nil {
@@ -4,7 +4,6 @@ import (
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -68,7 +67,10 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
calendarId := chi.URLParam(r, UriParamCalendarId)
calendarId, err := req.PathParam(UriParamCalendarId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
logger := log.From(l)
@@ -110,7 +112,10 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
l := req.logger.With()
calendarId := chi.URLParam(r, UriParamCalendarId)
calendarId, err := req.PathParam(UriParamCalendarId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
@@ -157,9 +162,6 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
l := req.logger.With()
calendarId := chi.URLParam(r, UriParamCalendarId)
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
var create jmap.CalendarEvent
err := req.body(&create)
if err != nil {
@@ -175,6 +177,7 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
})
}
// @api:tag XYZ
func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := req.needCalendarWithAccount()
@@ -183,9 +186,11 @@ func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request)
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
calendarId := chi.URLParam(r, UriParamCalendarId)
eventId := chi.URLParam(r, UriParamEventId)
l.Str(UriParamCalendarId, log.SafeString(calendarId)).Str(UriParamEventId, log.SafeString(eventId))
eventId, err := req.PathParam(UriParamEventId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEventId, log.SafeString(eventId))
logger := log.From(l)
@@ -220,7 +225,10 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), err)
}
blobId := chi.URLParam(r, UriParamBlobId)
blobId, err := req.PathParam(UriParamBlobId)
if err != nil {
return errorResponse(single(accountId), err)
}
blobIds := strings.Split(blobId, ",")
l := req.logger.With().Array(UriParamBlobId, log.SafeStringArray(blobIds))
@@ -3,7 +3,6 @@ package groupware
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/jscontact"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -68,7 +67,10 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
addressBookId := chi.URLParam(r, UriParamAddressBookId)
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
logger := log.From(l)
@@ -110,7 +112,10 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
l := req.logger.With()
addressBookId := chi.URLParam(r, UriParamAddressBookId)
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
@@ -157,7 +162,10 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
contactId := chi.URLParam(r, UriParamContactId)
contactId, err := req.PathParam(UriParamContactId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
@@ -183,11 +191,14 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
l := req.logger.With()
addressBookId := chi.URLParam(r, UriParamAddressBookId)
addressBookId, err := req.PathParam(UriParamAddressBookId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
var create jscontact.ContactCard
err := req.body(&create)
err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -209,7 +220,10 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(accountId, log.SafeString(accountId))
contactId := chi.URLParam(r, UriParamContactId)
contactId, err := req.PathParam(UriParamContactId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamContactId, log.SafeString(contactId))
logger := log.From(l)
@@ -11,7 +11,6 @@ import (
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/microcosm-cc/bluemonday"
"github.com/rs/zerolog"
@@ -39,6 +38,54 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
}
}
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails/since/{since} email get_all_emails_in_mailbox_since
// Get all the emails in a mailbox since a given state.
//
// Retrieve the list of all the emails that are in a given mailbox since a given state.
//
// The mailbox must be specified by its id, as part of the request URL path.
//
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
// respectively.
//
// responses:
//
// 200: GetAllEmailsInMailboxSince200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetAllEmailsInMailboxSince(w http.ResponseWriter, r *http.Request) {
maxChanges := uint(0)
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return errorResponse(single(accountId), err)
}
since, err := req.PathParamDoc(UriParamSince, "State identifier that indicates the coordinate from whence on to list mailbox changes")
if err != nil {
return errorResponse(single(accountId), err)
}
logger := log.From(req.logger.With().Str(HeaderParamSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
})
}
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails email get_all_emails_in_mailbox
// Get all the emails in a mailbox.
//
@@ -49,108 +96,79 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
// respectively.
//
// When the query parameter 'since' or the 'if-none-match' header is specified, then the
// request behaves differently, performing a changes query to determine what has changed in
// that mailbox since a given state identifier.
//
// responses:
//
// 200: GetAllEmailsInMailbox200
// 200: GetAllEmailsInMailboxSince200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
// 200: GetAllEmailsInMailbox200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
since := r.Header.Get(HeaderSince)
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
if since != "" {
// ... then it's a completely different operation
maxChanges := uint(0)
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(logAccountId, accountId)
if mailboxId == "" {
return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
}
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return errorResponse(single(accountId), err)
}
logger := log.From(req.logger.With().Str(HeaderSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
if err != nil {
return errorResponse(single(accountId), err)
}
if ok {
l = l.Int(QueryParamOffset, offset)
}
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
if err != nil {
return errorResponse(single(accountId), err)
}
if ok {
l = l.Uint(QueryParamLimit, limit)
}
return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
})
} else {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
logger := log.From(l)
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(logAccountId, accountId)
collapseThreads := false
fetchBodies := false
withThreads := true
if mailboxId == "" {
return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
}
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
if err != nil {
return errorResponse(single(accountId), err)
}
if ok {
l = l.Int(QueryParamOffset, offset)
}
sanitized, err := req.sanitizeEmails(emails.Emails)
if err != nil {
return errorResponseWithSessionState(single(accountId), err, sessionState)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
if err != nil {
return errorResponse(single(accountId), err)
}
if ok {
l = l.Uint(QueryParamLimit, limit)
}
safe := jmap.Emails{
Emails: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Offset: emails.Offset,
}
logger := log.From(l)
collapseThreads := false
fetchBodies := false
withThreads := true
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
}
sanitized, err := req.sanitizeEmails(emails.Emails)
if err != nil {
return errorResponseWithSessionState(single(accountId), err, sessionState)
}
safe := jmap.Emails{
Emails: sanitized,
Total: emails.Total,
Limit: emails.Limit,
Offset: emails.Offset,
}
return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
})
}
return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
})
}
func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, UriParamEmailId)
ids := strings.Split(id, ",")
accept := r.Header.Get("Accept")
if accept == "message/rfc822" {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
id, err := req.PathParam(UriParamEmailId)
if err != nil {
return err
}
ids := strings.Split(id, ",")
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))
}
@@ -194,6 +212,11 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
id, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
ids := strings.Split(id, ",")
if len(ids) < 1 {
return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids"))
}
@@ -244,8 +267,6 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, UriParamEmailId)
contextAppender := func(l zerolog.Context) zerolog.Context { return l }
q := r.URL.Query()
var attachmentSelector func(jmap.EmailBodyPart) bool = nil
@@ -274,6 +295,12 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
return errorResponse(single(accountId), err)
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
id, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
logger := log.From(l)
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
@@ -286,20 +313,29 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
if err != nil {
return errorResponseWithSessionState(single(accountId), err, sessionState)
}
return etagResponse(single(accountId), email.Attachments, sessionState, EmailResponseObjectType, state, lang)
var body []jmap.EmailBodyPart = email.Attachments
return etagResponse(single(accountId), body, sessionState, EmailResponseObjectType, state, lang)
})
} else {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
mailAccountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return gwerr
mailAccountId, err := req.GetAccountIdForMail()
if err != nil {
return err
}
blobAccountId, gwerr := req.GetAccountIdForBlob()
if gwerr != nil {
return gwerr
blobAccountId, err := req.GetAccountIdForBlob()
if err != nil {
return err
}
l := req.logger.With().Str(logAccountId, log.SafeString(mailAccountId)).Str(logBlobAccountId, log.SafeString(blobAccountId))
id, err := req.PathParam(UriParamEmailId)
if err != nil {
return err
}
l := req.logger.With().
Str(logAccountId, log.SafeString(mailAccountId)).
Str(logBlobAccountId, log.SafeString(blobAccountId)).
Str(UriParamEmailId, log.SafeString(id))
l = contextAppender(l)
logger := log.From(l)
@@ -434,25 +470,30 @@ type EmailSearchResults struct {
QueryState jmap.State `json:"queryState,omitempty"`
}
func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
q := req.r.URL.Query()
mailboxId := q.Get(QueryParamMailboxId)
notInMailboxIds := q[QueryParamNotInMailboxId]
text := q.Get(QueryParamSearchText)
from := q.Get(QueryParamSearchFrom)
to := q.Get(QueryParamSearchTo)
cc := q.Get(QueryParamSearchCc)
bcc := q.Get(QueryParamSearchBcc)
subject := q.Get(QueryParamSearchSubject)
body := q.Get(QueryParamSearchBody)
keywords := q[QueryParamSearchKeyword]
messageId := q.Get(QueryParamSearchMessageId)
func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
mailboxId, _ := req.getStringParam(QueryParamMailboxId, "") // the identifier of the Mailbox to which to restrict the search
text, _ := req.getStringParam(QueryParamSearchText, "") // text that must be included in the Email, specifically in From, To, Cc, Bcc, Subject and any text/* body part
from, _ := req.getStringParam(QueryParamSearchFrom, "") // text that must be included in the From header of the Email
to, _ := req.getStringParam(QueryParamSearchTo, "") // text that must be included in the To header of the Email
cc, _ := req.getStringParam(QueryParamSearchCc, "") // text that must be included in the Cc header of the Email
bcc, _ := req.getStringParam(QueryParamSearchBcc, "") // text that must be included in the Bcc header of the Email
subject, _ := req.getStringParam(QueryParamSearchSubject, "") // text that must be included in the Subject of the Email
body, _ := req.getStringParam(QueryParamSearchBody, "") // text that must be included in any text/* part of the body of the Email
messageId, _ := req.getStringParam(QueryParamSearchMessageId, "") // value of the Message-ID header of the Email
notInMailboxIds, _, err := req.parseOptStringListParam(QueryParamNotInMailboxId) // a comma-separated list of identifiers of Mailboxes the Email must *not* be in
if err != nil {
return false, nil, false, 0, 0, nil, err
}
keywords, _, err := req.parseOptStringListParam(QueryParamSearchKeyword) // the Email must have all those keywords
if err != nil {
return false, nil, false, 0, 0, nil, err
}
snippets := false
l := req.logger.With()
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
offset, ok, err := req.parseIntParam(QueryParamOffset, 0) // pagination element offset
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -460,7 +501,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Int(QueryParamOffset, offset)
}
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) // maximum number of results (size of a page)
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -468,7 +509,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Uint(QueryParamLimit, limit)
}
before, ok, err := req.parseDateParam(QueryParamSearchBefore)
before, ok, err := req.parseDateParam(QueryParamSearchBefore) // the Email must have been received before this date-time
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -476,7 +517,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Time(QueryParamSearchBefore, before)
}
after, ok, err := req.parseDateParam(QueryParamSearchAfter)
after, ok, err := req.parseDateParam(QueryParamSearchAfter) // the Email must have been received after this date-time
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -515,7 +556,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Str(QueryParamSearchMessageId, log.SafeString(messageId))
}
minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0)
minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0) // the minimum size of the Email
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -523,7 +564,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
l = l.Int(QueryParamSearchMinSize, minSize)
}
maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0)
maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0) // the maximum size of the Email
if err != nil {
return false, nil, snippets, 0, 0, nil, err
}
@@ -586,7 +627,7 @@ 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)
since = r.Header.Get(HeaderParamSince)
}
if since != "" {
// get email changes since a given state
@@ -599,7 +640,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), err)
}
ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return errorResponse(single(accountId), err)
}
@@ -658,7 +699,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
g.respond(w, r, func(req Request) Response {
allAccountIds := req.AllAccountIds()
ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
if !ok {
return errorResponse(allAccountIds, err)
}
@@ -698,12 +739,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
// TODO offset and limit over the aggregated results by account
return etagResponse(allAccountIds, EmailSearchSnippetsResults{
body := EmailSearchSnippetsResults{
Results: flattened,
Total: totalOverAllAccounts,
Limit: limit,
QueryState: state,
}, sessionState, EmailResponseObjectType, state, lang)
}
return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
} else {
withThreads := true
@@ -735,12 +778,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
// TODO offset and limit over the aggregated results by account
return etagResponse(allAccountIds, EmailSearchResults{
body := EmailSearchResults{
Results: flattened,
Total: totalAcrossAllAccounts,
Limit: limit,
QueryState: state,
}, sessionState, EmailResponseObjectType, state, lang)
}
return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
}
})
}
@@ -845,12 +890,15 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
return errorResponse(single(accountId), gwerr)
}
replaceId := chi.URLParam(r, UriParamEmailId)
replaceId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
var body jmap.EmailCreate
err := req.body(&body)
err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -883,10 +931,7 @@ type SwaggerUpdateEmailBody struct {
func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
emailId := chi.URLParam(r, UriParamEmailId)
l := req.logger.With()
l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -894,10 +939,16 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
var body map[string]any
err := req.body(&body)
err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -936,10 +987,7 @@ func (e emailKeywordUpdates) IsEmpty() bool {
func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
emailId := chi.URLParam(r, UriParamEmailId)
l := req.logger.With()
l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -947,10 +995,16 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
}
l.Str(logAccountId, accountId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
var body emailKeywordUpdates
err := req.body(&body)
err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1000,10 +1054,7 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
// 500: ErrorResponse500
func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
emailId := chi.URLParam(r, UriParamEmailId)
l := req.logger.With()
l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -1011,10 +1062,16 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
var body []string
err := req.body(&body)
err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1065,21 +1122,24 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
// 500: ErrorResponse500
func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
emailId := chi.URLParam(r, UriParamEmailId)
l := req.logger.With()
l.Str(UriParamEmailId, log.SafeString(emailId))
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return errorResponse(single(accountId), gwerr)
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(logAccountId, accountId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
var body []string
err := req.body(&body)
err = req.body(&body)
if err != nil {
return errorResponse(single(accountId), err)
}
@@ -1130,16 +1190,19 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
// 500: ErrorResponse500
func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
emailId := chi.URLParam(r, UriParamEmailId)
l := req.logger.With()
l.Str(UriParamEmailId, emailId)
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
return errorResponse(single(accountId), gwerr)
}
l.Str(logAccountId, accountId)
l.Str(logAccountId, log.SafeString(accountId))
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
logger := log.From(l)
@@ -1188,6 +1251,7 @@ type SwaggerDeleteEmailsBody struct {
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
/// @api body
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
@@ -1237,7 +1301,10 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) {
}
l.Str(logAccountId, accountId)
emailId := chi.URLParam(r, UriParamEmailId)
emailId, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l.Str(UriParamEmailId, log.SafeString(emailId))
identityId, err := req.getMandatoryStringParam(QueryParamIdentityId)
@@ -1333,10 +1400,8 @@ func relatedEmailsFilter(email jmap.Email, beacon time.Time, days uint) jmap.Ema
}
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))
l := req.logger.With()
accountId, gwerr := req.GetAccountIdForMail()
if gwerr != nil {
@@ -1344,6 +1409,12 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, log.SafeString(accountId))
id, err := req.PathParam(UriParamEmailId)
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(logEmailId, log.SafeString(id))
limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit
if err != nil {
return errorResponse(single(accountId), err)
@@ -1715,7 +1786,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
return errorResponse(allAccountIds, err)
}
if offset > 0 {
return notImplementesResponse()
return notImplementedResponse()
}
if ok {
l = l.Uint(QueryParamOffset, limit)
@@ -5,7 +5,6 @@ import (
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
@@ -48,7 +47,10 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
if err != nil {
return errorResponse(single(accountId), err)
}
id := chi.URLParam(r, UriParamIdentityId)
id, err := req.PathParam(UriParamIdentityId)
if err != nil {
return errorResponse(single(accountId), err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id})
if jerr != nil {
@@ -57,7 +59,8 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
if len(res) < 1 {
return notFoundResponse(single(accountId), sessionState)
}
return etagResponse(single(accountId), res[0], sessionState, IdentityResponseObjectType, state, lang)
var body jmap.Identity = res[0]
return etagResponse(single(accountId), body, sessionState, IdentityResponseObjectType, state, lang)
})
}
@@ -105,6 +108,7 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
})
}
// Delete an identity.
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
@@ -113,10 +117,13 @@ func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
id := chi.URLParam(r, UriParamIdentityId)
id, err := req.PathParam(UriParamIdentityId)
if err != nil {
return errorResponse(single(accountId), err)
}
ids := strings.Split(id, ",")
if len(ids) < 1 {
return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
return req.parameterErrorResponse(single(accountId), UriParamIdentityId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
}
deletion, sessionState, state, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
@@ -153,6 +153,7 @@ type SwaggerIndexResponse struct {
// swagger:route GET /groupware bootstrap index
// Get initial bootstrapping information for a user.
// @api:tag bootstrap
//
// responses:
//
@@ -166,13 +167,14 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
return req.errorResponseFromJmap(accountIds, err)
}
return etagResponse(accountIds, IndexResponse{
var RBODY IndexResponse = IndexResponse{
Version: Version,
Capabilities: Capabilities,
Limits: buildIndexLimits(req.session),
Accounts: buildIndexAccounts(req.session, boot),
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
}, sessionState, IndexResponseObjectType, state, lang)
}
return etagResponse(accountIds, RBODY, sessionState, IndexResponseObjectType, state, lang)
})
}
@@ -5,7 +5,6 @@ import (
"slices"
"strings"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/opencloud-eu/opencloud/pkg/jmap"
@@ -35,13 +34,17 @@ type SwaggerGetMailboxById200 struct {
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
g.respond(w, r, func(req Request) Response {
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
}
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return errorResponse(single(accountId), err)
}
mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId})
if jerr != nil {
return req.errorResponseFromJmap(single(accountId), jerr)
@@ -92,22 +95,21 @@ type SwaggerMailboxesResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
var filter jmap.MailboxFilterCondition
hasCriteria := false
name := q.Get(QueryParamMailboxSearchName)
if name != "" {
filter.Name = name
hasCriteria = true
}
role := q.Get(QueryParamMailboxSearchRole)
if role != "" {
filter.Role = role
hasCriteria = true
}
g.respond(w, r, func(req Request) Response {
var filter jmap.MailboxFilterCondition
hasCriteria := false
name, ok := req.getStringParam(QueryParamMailboxSearchName, "") // the mailbox name to filter on
if ok && name != "" {
filter.Name = name
hasCriteria = true
}
role, ok := req.getStringParam(QueryParamMailboxSearchRole, "") // the mailbox role to filter on
if role != "" {
filter.Role = role
hasCriteria = true
}
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(single(accountId), err)
@@ -157,8 +159,7 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
}
// swagger:route GET /groupware/accounts/all/mailboxes mailboxesforallaccounts mailbox
// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the
// role of the mailboxes.
// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the role of the mailboxes.
//
// responses:
//
@@ -166,28 +167,24 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
var filter jmap.MailboxFilterCondition
hasCriteria := false
role := q.Get(QueryParamMailboxSearchRole)
if role != "" {
filter.Role = role
hasCriteria = true
}
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
return noContentResponse(nil, "")
return noContentResponse(nil, "") // when the user has no accounts
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false)
if err != nil {
return errorResponse(accountIds, err)
var filter jmap.MailboxFilterCondition
hasCriteria := false
if role, set := req.getStringParam(QueryParamMailboxSearchRole, ""); set {
filter.Role = role
hasCriteria = true
}
if set {
if subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false); err != nil {
return errorResponse(accountIds, err)
} else if set {
filter.IsSubscribed = &subscribed
hasCriteria = true
}
@@ -208,22 +205,28 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
})
}
// Retrieve Mailboxes by their role for all accounts.
func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *http.Request) {
role := chi.URLParam(r, UriParamRole)
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
return noContentResponse(nil, "")
return noContentResponse(nil, "") // when the user has no accounts
}
role, err := req.PathParamDoc(UriParamRole, "Role of the mailboxes to retrieve across all accounts")
if err != nil {
return errorResponse(accountIds, err)
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)).Str("role", role))
filter := jmap.MailboxFilterCondition{
Role: role,
}
mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
if err != nil {
return req.errorResponseFromJmap(accountIds, err)
mailboxesByAccountId, sessionState, state, lang, jerr := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
if jerr != nil {
return req.errorResponseFromJmap(accountIds, jerr)
}
return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
})
@@ -245,11 +248,8 @@ type SwaggerMailboxChangesResponse200 struct {
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
sinceState := r.Header.Get(HeaderSince)
g.respond(w, r, func(req Request) Response {
l := req.logger.With().Str(HeaderSince, sinceState)
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
@@ -257,6 +257,11 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
mailboxId, err := req.PathParam(UriParamMailboxId)
if err != nil {
return errorResponse(single(accountId), err)
}
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return errorResponse(single(accountId), err)
@@ -265,6 +270,12 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes")
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(HeaderParamSince, log.SafeString(sinceState))
logger := log.From(l)
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.config.maxBodyValueBytes, maxChanges)
@@ -329,6 +340,8 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
})
}
// Retrieve the roles of all the Mailboxes of all Accounts.
// @api:example mailboxrolesbyaccount
func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
@@ -346,10 +359,8 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
g.respond(w, r, func(req Request) Response {
l := req.logger.With().Str(UriParamMailboxId, log.SafeString(mailboxId))
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
if err != nil {
@@ -357,6 +368,12 @@ func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
mailboxId, err := req.PathParamDoc(UriParamMailboxId, "the identifier of the mailbox to update")
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Str(UriParamMailboxId, log.SafeString(mailboxId))
var body jmap.MailboxChange
err = req.body(&body)
if err != nil {
@@ -398,10 +415,12 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
})
}
// Delete Mailboxes by their unique identifiers.
//
// Returns the identifiers of the Mailboxes that have successfully been deleted.
//
// @api:example deletedmailboxes
func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
mailboxIds := strings.Split(mailboxId, ",")
g.respond(w, r, func(req Request) Response {
l := req.logger.With()
accountId, err := req.GetAccountIdForMail()
@@ -410,11 +429,16 @@ func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
}
l = l.Str(logAccountId, accountId)
mailboxIds, err := req.PathListParamDoc(UriParamMailboxId, "the identifier of the mailbox to delete")
if err != nil {
return errorResponse(single(accountId), err)
}
l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
if len(mailboxIds) < 1 {
return noContentResponse(single(accountId), req.session.State)
return noContentResponse(single(accountId), req.session.State) // no mailbox identifiers were mentioned in the request
}
l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
logger := log.From(l)
deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, req.session, req.ctx, logger, req.language(), "", mailboxIds)
@@ -441,8 +465,8 @@ func scoreMailbox(m jmap.Mailbox) int {
return 1000
}
func sortMailboxesMap[K comparable](mailboxesByAccountId map[K][]jmap.Mailbox) map[K][]jmap.Mailbox {
sortedByAccountId := make(map[K][]jmap.Mailbox, len(mailboxesByAccountId))
func sortMailboxesMap(mailboxesByAccountId map[string][]jmap.Mailbox) map[string][]jmap.Mailbox {
sortedByAccountId := make(map[string][]jmap.Mailbox, len(mailboxesByAccountId))
for accountId, unsorted := range mailboxesByAccountId {
mailboxes := make([]jmap.Mailbox, len(unsorted))
copy(mailboxes, unsorted)
@@ -17,6 +17,10 @@ type SwaggerGetQuotaResponse200 struct {
// swagger:route GET /groupware/accounts/{account}/quota quota get_quota
// Get quota limits.
//
// Retrieves the list of Quota configurations for a given account.
//
// Note that there may be multiple Quota objects for different resource types.
//
// responses:
//
// 200: GetQuotaResponse200
@@ -35,7 +39,8 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) {
return req.errorResponseFromJmap(single(accountId), jerr)
}
for _, v := range res {
return etagResponse(single(accountId), v.List, sessionState, QuotaResponseObjectType, state, lang)
body := v.List
return etagResponse(single(accountId), body, sessionState, QuotaResponseObjectType, state, lang)
}
return notFoundResponse(single(accountId), sessionState)
})
@@ -56,6 +61,9 @@ type SwaggerGetQuotaForAllAccountsResponse200 struct {
// swagger:route GET /groupware/accounts/all/quota quota get_quota_for_all_accounts
// Get quota limits for all accounts.
//
// Retrieves the Quota configuration for all the accounts the user currently has access to,
// as a dictionary that has the account identifier as its key and an array of Quotas as its value.
//
// responses:
//
// 200: GetQuotaForAllAccountsResponse200
@@ -65,7 +73,7 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
g.respond(w, r, func(req Request) Response {
accountIds := req.AllAccountIds()
if len(accountIds) < 1 {
return noContentResponse(accountIds, "")
return noContentResponse(accountIds, "") // user has no accounts
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
@@ -3,7 +3,6 @@ package groupware
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
@@ -31,7 +30,8 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
return etagResponse(single(accountId), AllTaskLists, req.session.State, TaskListResponseObjectType, TaskListsState, "")
var body []jmap.TaskList = AllTaskLists
return etagResponse(single(accountId), body, req.session.State, TaskListResponseObjectType, TaskListsState, "")
})
}
@@ -61,7 +61,10 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
tasklistId := chi.URLParam(r, UriParamTaskListId)
tasklistId, err := req.PathParam(UriParamTaskListId)
if err != nil {
return errorResponse(single(accountId), err)
}
// TODO replace with proper implementation
for _, tasklist := range AllTaskLists {
if tasklist.Id == tasklistId {
@@ -96,7 +99,10 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
}
var _ string = accountId
tasklistId := chi.URLParam(r, UriParamTaskListId)
tasklistId, err := req.PathParam(UriParamTaskListId)
if err != nil {
return errorResponse(single(accountId), err)
}
// TODO replace with proper implementation
tasks, ok := TaskMapByTaskListId[tasklistId]
if !ok {
@@ -194,7 +194,7 @@ const (
ErrorCodeMissingContactsSessionCapability = "MSCCON"
ErrorCodeMissingContactsAccountCapability = "MACCON"
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
ErrorCodeMissingTaskAccountCapability = "MACTSK"
ErrorCodeMissingTasksAccountCapability = "MACTSK"
ErrorCodeFailedToDeleteEmail = "DELEML"
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
ErrorCodeFailedToSanitizeEmail = "FSANEM"
@@ -318,12 +318,6 @@ var (
Title: "Invalid Request",
Detail: "The request is invalid.",
}
ErrorIndeterminateAccount = GroupwareError{
Status: http.StatusBadRequest,
Code: ErrorCodeNonExistingAccount,
Title: "Invalid Account Parameter",
Detail: "The account the request is for does not exist.",
}
ErrorNonExistingAccount = GroupwareError{
Status: http.StatusBadRequest,
Code: ErrorCodeIndeterminateAccount,
@@ -392,39 +386,39 @@ var (
}
ErrorMissingCalendarsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingCalendarsSessionCapability,
Title: "Session is missing the task capability '" + jmap.JmapCalendars + "'",
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
Code: ErrorCodeMissingCalendarsAccountCapability,
Title: "Session is missing the calendars session capability",
Detail: "The JMAP Session of the user does not have the required capability for calendars.",
}
ErrorMissingCalendarsAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingCalendarsSessionCapability,
Title: "Account is missing the task capability '" + jmap.JmapCalendars + "'",
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
Title: "Account is missing the calendars capability",
Detail: "The JMAP Account of the user does not have the required capability for calendars.",
}
ErrorMissingContactsSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingContactsSessionCapability,
Title: "Session is missing the task capability '" + jmap.JmapContacts + "'",
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapContacts + "'.",
Code: ErrorCodeMissingContactsAccountCapability,
Title: "Session is missing the contacts capability",
Detail: "The JMAP Session of the user does not have the required capability for accounts.",
}
ErrorMissingContactsAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingContactsSessionCapability,
Title: "Account is missing the task capability '" + jmap.JmapContacts + "'",
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapContacts + "'.",
Code: ErrorCodeMissingContactsAccountCapability,
Title: "Account is missing the contacts capability",
Detail: "The JMAP Account of the user does not have the required capability for accounts.",
}
ErrorMissingTasksSessionCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingTasksSessionCapability,
Title: "Session is missing the task capability '" + jmap.JmapTasks + "'",
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
Title: "Session is missing the tasks capability",
Detail: "The JMAP Session of the user does not have the required capability for tasks.",
}
ErrorMissingTasksAccountCapability = GroupwareError{
Status: http.StatusExpectationFailed,
Code: ErrorCodeMissingTasksSessionCapability,
Title: "Account is missing the task capability '" + jmap.JmapTasks + "'",
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
Code: ErrorCodeMissingTasksAccountCapability,
Title: "Account is missing the tasks capability",
Detail: "The JMAP Account of the user does not have the required capability for tasks",
}
ErrorFailedToDeleteEmail = GroupwareError{
Status: http.StatusInternalServerError,
@@ -0,0 +1,219 @@
//go:build groupware_examples
package groupware
import (
"time"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
var (
exampleQuotaState = "veiv8iez"
)
type Exampler struct{}
var j = jmap.ExamplerInstance
func Example() {
jmap.SerializeExamples(Exampler{})
//Output:
}
func (e Exampler) AccountQuota() AccountQuota {
return AccountQuota{
Quotas: []jmap.Quota{j.Quota()},
State: jmap.State(exampleQuotaState),
}
}
func (e Exampler) AccountQuotaMap() map[string]AccountQuota {
return map[string]AccountQuota{
j.AccountId: e.AccountQuota(),
}
}
func (e Exampler) AccountWithId() AccountWithId {
a, _ := j.Account()
return AccountWithId{
AccountId: j.AccountId,
Account: a,
}
}
func (e Exampler) AccountWithIdAndIdentities() AccountWithIdAndIdentities {
a, _ := j.Account()
return AccountWithIdAndIdentities{
AccountId: j.AccountId,
Account: a,
Identities: j.Identities(),
}
}
func (e Exampler) IndexAccountMailCapabilities() IndexAccountMailCapabilities {
m := j.SessionMailAccountCapabilities()
s := j.SessionSubmissionAccountCapabilities()
return IndexAccountMailCapabilities{
MaxMailboxDepth: m.MaxMailboxDepth,
MaxSizeMailboxName: m.MaxSizeMailboxName,
MaxMailboxesPerEmail: m.MaxMailboxesPerEmail,
MaxSizeAttachmentsPerEmail: m.MaxSizeAttachmentsPerEmail,
MayCreateTopLevelMailbox: m.MayCreateTopLevelMailbox,
MaxDelayedSend: s.MaxDelayedSend,
}
}
func (e Exampler) IndexAccountSieveCapabilities() IndexAccountSieveCapabilities {
s := j.SessionSieveAccountCapabilities()
return IndexAccountSieveCapabilities{
MaxSizeScriptName: s.MaxSizeScriptName,
MaxSizeScript: s.MaxSizeScript,
MaxNumberScripts: s.MaxNumberScripts,
MaxNumberRedirects: s.MaxNumberRedirects,
}
}
func (e Exampler) IndexAccountCapabilities() IndexAccountCapabilities {
return IndexAccountCapabilities{
Mail: e.IndexAccountMailCapabilities(),
Sieve: e.IndexAccountSieveCapabilities(),
}
}
func (e Exampler) IndexAccount() IndexAccount {
a, _ := j.Account()
return IndexAccount{
AccountId: j.AccountId,
Name: a.Name,
IsPersonal: a.IsPersonal,
IsReadOnly: a.IsReadOnly,
Capabilities: e.IndexAccountCapabilities(),
Identities: j.Identities(),
Quotas: j.Quotas(),
}
}
func (e Exampler) IndexAccounts() []IndexAccount {
return []IndexAccount{
e.IndexAccount(),
{
AccountId: j.SharedAccountId,
Name: j.SharedAccountName,
IsPersonal: false,
IsReadOnly: true,
Capabilities: e.IndexAccountCapabilities(),
Identities: []jmap.Identity{
{
Id: j.SharedIdentityId,
Name: j.SharedIdentityName,
Email: j.SharedIdentityEmailAddress,
},
},
Quotas: j.Quotas(),
},
}
}
func (e Exampler) IndexPrimaryAccounts() IndexPrimaryAccounts {
return IndexPrimaryAccounts{
Mail: j.AccountId,
Submission: j.AccountId,
Blob: j.AccountId,
VacationResponse: j.AccountId,
Sieve: j.AccountId,
}
}
func (e Exampler) IndexResponse() IndexResponse {
return IndexResponse{
Version: "4.0.0",
Capabilities: []string{"mail:1"},
Limits: IndexLimits{
MaxSizeUpload: 50000000,
MaxConcurrentUpload: 4,
MaxSizeRequest: 10000000,
MaxConcurrentRequests: 4,
},
PrimaryAccounts: e.IndexPrimaryAccounts(),
Accounts: []IndexAccount{e.IndexAccount()},
}
}
func (e Exampler) ErrorResponse() ErrorResponse {
err := apiError("6d9c65d1-0368-4833-b09f-885aa0171b95", ErrorNoMailboxWithDraftRole)
return ErrorResponse{
Errors: []Error{
*err,
},
}
}
func (e Exampler) MailboxesByAccountId() (map[string][]jmap.Mailbox, string) {
j := jmap.ExamplerInstance
return map[string][]jmap.Mailbox{
j.AccountId: j.Mailboxes(),
}, "All mailboxes for all accounts, without a role filter"
}
func (e Exampler) MailboxesByAccountIdFilteredOnInboxRole() (map[string][]jmap.Mailbox, string, string) {
j := jmap.ExamplerInstance
return map[string][]jmap.Mailbox{
j.AccountId: structs.Filter(j.Mailboxes(), func(m jmap.Mailbox) bool { return m.Role == jmap.JmapMailboxRoleInbox }),
}, "All mailboxes for all accounts, filtered on the 'inbox' role", "inboxrole"
}
func (e Exampler) EmailSearchResults() EmailSearchResults {
sent, _ := time.Parse(time.RFC3339, "2026-01-12T21:46:01Z")
received, _ := time.Parse(time.RFC3339, "2026-01-12T21:47:21Z")
j := jmap.ExamplerInstance
return EmailSearchResults{
Results: []jmap.Email{
{
Id: "ov7ienge",
BlobId: "ccyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasiba",
ThreadId: "is",
MailboxIds: map[string]bool{j.MailboxInboxId: true},
Keywords: map[string]bool{jmap.JmapKeywordAnswered: true},
Size: 1084,
ReceivedAt: received,
MessageId: []string{"1768845021.1753110@example.com"},
Sender: []jmap.EmailAddress{
{Name: j.SenderName, Email: j.SenderEmailAddress},
},
From: []jmap.EmailAddress{
{Name: j.SenderName, Email: j.SenderEmailAddress},
},
To: []jmap.EmailAddress{
{Name: j.IdentityName, Email: j.EmailAddress},
},
Subject: "Remember the Cant",
SentAt: sent,
TextBody: []jmap.EmailBodyPart{
{PartId: "1", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnebdw", Size: 115, Type: "text/plain", Charset: "utf-8"},
},
HtmlBody: []jmap.EmailBodyPart{
{PartId: "2", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnsbvjae", Size: 163, Type: "text/html", Charset: "utf-8"},
},
Preview: "The Canterbury was destroyed while investigating a false distress call from the Scopuli.",
},
},
Total: 132,
Limit: 1,
QueryState: "seehug3p",
}
}
func (e Exampler) MailboxRolesByAccounts() (map[string][]string, string, string, string) {
j := jmap.ExamplerInstance
return map[string][]string{
j.AccountId: jmap.JmapMailboxRoles,
j.SharedAccountId: jmap.JmapMailboxRoles,
}, "Roles of the Mailboxes of each Account", "", "mailboxrolesbyaccount"
}
func (e Exampler) DeletedMailboxes() ([]string, string, string, string) {
j := jmap.ExamplerInstance
return []string{j.MailboxProjectId, j.MailboxJunkId}, "Identifiers of the Mailboxes that have successfully been deleted", "", "deletedmailboxes"
}
@@ -68,14 +68,64 @@ var (
errNoPrimaryAccountForBlob = errors.New("no primary account for blob")
errNoPrimaryAccountForVacationResponse = errors.New("no primary account for vacation response")
errNoPrimaryAccountForSubmission = errors.New("no primary account for submission")
errNoPrimaryAccountForTask = errors.New("no primary account for task")
errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
errNoPrimaryAccountForContact = errors.New("no primary account for contact")
errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
// errNoPrimaryAccountForTask = errors.New("no primary account for task")
// errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
// errNoPrimaryAccountForContact = errors.New("no primary account for contact")
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
)
func (r Request) HeaderParam(name string) (string, *Error) {
value := r.r.Header.Get(name)
if value == "" {
msg := fmt.Sprintf("Missing mandatory request header '%s'", name)
return "", r.observedParameterError(ErrorInvalidRequestParameter,
withDetail(msg),
withSource(&ErrorSource{Header: name}),
)
} else {
return value, nil
}
}
func (r Request) HeaderParamDoc(name string, _ string) (string, *Error) {
return r.HeaderParam(name)
}
func (r Request) OptHeaderParam(name string) string {
return r.r.Header.Get(name)
}
func (r Request) OptHeaderParamDoc(name string, _ string) string {
return r.OptHeaderParam(name)
}
func (r Request) PathParam(name string) (string, *Error) {
value := chi.URLParam(r.r, name)
if value == "" {
msg := fmt.Sprintf("Missing mandatory path parameter '%s'", name)
return "", r.observedParameterError(ErrorInvalidRequestParameter,
withDetail(msg),
withSource(&ErrorSource{Parameter: name}),
)
} else {
return value, nil
}
}
func (r Request) PathParamDoc(name string, _ string) (string, *Error) {
return r.PathParam(name)
}
func (r Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
value, err := r.PathParam(name)
if err != nil {
return nil, err
}
return strings.Split(value, ","), nil
}
func (r Request) AllAccountIds() []string {
// TODO potentially filter on "subscribed" accounts?
return structs.Uniq(structs.Keys(r.session.Accounts))
@@ -313,6 +363,26 @@ func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
return result, true, nil
}
func (r Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
result := []string{}
q := r.r.URL.Query()
if !q.Has(param) {
return nil, false, nil
}
for _, value := range q[param] {
for _, v := range strings.Split(value, ",") {
if strings.TrimSpace(v) != "" {
result = append(result, v)
}
}
}
return result, true, nil
}
func (r Request) bodydoc(target any, _ string) *Error {
return r.body(target)
}
func (r Request) body(target any) *Error {
body := r.r.Body
defer func(b io.ReadCloser) {
@@ -163,7 +163,7 @@ func etagNotFoundResponse(accountIds []string, sessionState jmap.SessionState, o
}
}
func notImplementesResponse() Response {
func notImplementedResponse() Response {
return Response{
body: nil,
status: http.StatusNotImplemented,
@@ -11,19 +11,20 @@ var (
)
const (
UriParamAccountId = "accountid"
UriParamMailboxId = "mailboxid"
UriParamEmailId = "emailid"
UriParamIdentityId = "identityid"
UriParamBlobId = "blobid"
UriParamAccountId = "accountid" // Identifier of the account
UriParamMailboxId = "mailboxid" // Identifier of the mailbox
UriParamEmailId = "emailid" // Identifier of the email
UriParamIdentityId = "identityid" // Identifier of the identity
UriParamBlobId = "blobid" // Identifier of theblob
UriParamStreamId = "stream" // Identifier of the stream
UriParamAddressBookId = "addressbookid" // Identifier of the address book
UriParamCalendarId = "calendarid" // Identifier of the calendar
UriParamTaskListId = "tasklistid" // Identifier of the tasklist
UriParamContactId = "contactid" // Identifier of the contact
UriParamEventId = "eventid" // Idenfitier of the event
UriParamBlobName = "blobname"
UriParamStreamId = "stream"
UriParamSince = "since"
UriParamRole = "role"
UriParamAddressBookId = "addressbookid"
UriParamCalendarId = "calendarid"
UriParamTaskListId = "tasklistid"
UriParamContactId = "contactid"
UriParamEventId = "eventid"
QueryParamMailboxSearchName = "name"
QueryParamMailboxSearchRole = "role"
QueryParamMailboxSearchSubscribed = "subscribed"
@@ -57,7 +58,7 @@ const (
QueryParamSeen = "seen"
QueryParamUndesirable = "undesirable"
QueryParamMarkAsSeen = "markAsSeen"
HeaderSince = "if-none-match"
HeaderParamSince = "if-none-match"
)
func (g *Groupware) Route(r chi.Router) {
@@ -100,6 +101,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Route("/{mailboxid}", func(r chi.Router) {
r.Get("/", g.GetMailbox)
r.Get("/emails", g.GetAllEmailsInMailbox)
r.Get("/emails/since/{since}", g.GetAllEmailsInMailboxSince)
r.Get("/changes", g.GetMailboxChanges)
r.Patch("/", g.UpdateMailbox)
r.Delete("/", g.DeleteMailbox)
+22 -4
View File
@@ -29,7 +29,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
opt := authOptions(opts...)
tokenManager, err := jwt.New(map[string]any{
"secret": opt.JWTSecret,
"expires": int64(24 * 60 * 60),
"expires": int64(24 * 60 * 60), // token expiration in seconds
})
if err != nil {
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
@@ -39,19 +39,37 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
ctx := r.Context()
t := r.Header.Get(revactx.TokenHeader)
if t == "" {
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Msgf("missing access token in header %v", revactx.TokenHeader)
requestID := r.Header.Get("X-Request-ID")
traceID := GetTraceID(ctx)
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
if traceID != "" {
l = l.Str(LogTraceID, log.SafeString(traceID))
}
l.Msgf("missing access token in header %v", revactx.TokenHeader)
w.WriteHeader(http.StatusUnauthorized) // missing access token
return
}
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t)
if err != nil {
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
requestID := r.Header.Get("X-Request-ID")
traceID := GetTraceID(ctx)
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
if traceID != "" {
l = l.Str(LogTraceID, log.SafeString(traceID))
}
l.Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
w.WriteHeader(http.StatusUnauthorized) // invalid token
return
}
if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok {
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msg("verifying scope failed")
requestID := r.Header.Get("X-Request-ID")
traceID := GetTraceID(ctx)
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
if traceID != "" {
l = l.Str(LogTraceID, log.SafeString(traceID))
}
l.Err(err).Msg("verifying scope failed")
w.WriteHeader(http.StatusUnauthorized) // invalid scope
return
}
@@ -8,6 +8,10 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
)
var (
LogTraceID = "traceId"
)
func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -37,12 +41,10 @@ func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
ctx := r.Context()
requestID := middleware.GetReqID(ctx)
level = level.Str(log.RequestIDString, log.SafeString(requestID))
traceID := GetTraceID(ctx)
level.Str(log.RequestIDString, requestID)
if traceID != "" {
level.Str("traceId", traceID)
level = level.Str(LogTraceID, log.SafeString(traceID))
}
level.