mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-08 04:20:59 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user