mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-13 09:02:09 -04:00
* split off the Stalwart integration test boilerplate into a new jmaptest package, in order to be able to use it from the groupware package as well * remove the JMAP Session patching in tests and, instead, pick random free local ports for HTTP and IMAPS manually, and set those explicitly in the Stalwart testcontainer, so that we can set the PUBLIC_URL environment variable that Stalwart then uses to define the URLs that are served as part of the JMAP Sessions (API URL, etc...)
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
package jmap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/url"
|
|
"slices"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/pkg/structs"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type Client struct {
|
|
session SessionClient
|
|
api ApiClient
|
|
blob BlobClient
|
|
ws WsClientFactory
|
|
sessionEventListeners *eventListeners[SessionEventListener]
|
|
wsPushListeners *eventListeners[WsPushListener]
|
|
io.Closer
|
|
WsPushListener
|
|
}
|
|
|
|
type ApiSupplier interface {
|
|
Api() ApiClient
|
|
}
|
|
|
|
type Hooks interface {
|
|
OnSessionOutdated(session *Session, newState SessionState)
|
|
}
|
|
|
|
var _ io.Closer = &Client{}
|
|
var _ WsPushListener = &Client{}
|
|
var _ ApiSupplier = &Client{}
|
|
var _ Hooks = &Client{}
|
|
|
|
func (j *Client) Close() error {
|
|
return errors.Join(j.api.Close(), j.session.Close(), j.blob.Close(), j.ws.Close())
|
|
}
|
|
|
|
func (j *Client) Api() ApiClient {
|
|
return j.api
|
|
}
|
|
|
|
func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) *Client {
|
|
return &Client{
|
|
session: session,
|
|
api: api,
|
|
blob: blob,
|
|
ws: ws,
|
|
sessionEventListeners: newEventListeners[SessionEventListener](),
|
|
wsPushListeners: newEventListeners[WsPushListener](),
|
|
}
|
|
}
|
|
|
|
func (j *Client) AddSessionEventListener(listener SessionEventListener) {
|
|
j.sessionEventListeners.add(listener)
|
|
}
|
|
|
|
func (j *Client) OnSessionOutdated(session *Session, newSessionState SessionState) {
|
|
j.sessionEventListeners.signal(func(listener SessionEventListener) {
|
|
listener.OnSessionOutdated(session, newSessionState)
|
|
})
|
|
}
|
|
|
|
func (j *Client) OnNotification(username string, stateChange StateChange) {
|
|
j.wsPushListeners.signal(func(listener WsPushListener) {
|
|
listener.OnNotification(username, stateChange)
|
|
})
|
|
}
|
|
|
|
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
|
|
func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
|
|
sessionResponse, err := j.session.GetSession(ctx, sessionUrl, username, logger)
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
return newSession(sessionResponse)
|
|
}
|
|
|
|
func (j *Client) logger(operation string, ctx Context) *log.Logger {
|
|
l := ctx.Logger.With().Str(logOperation, operation)
|
|
return log.From(l)
|
|
}
|
|
|
|
func (j *Client) loggerParams(operation string, ctx Context, params func(zerolog.Context) zerolog.Context) *log.Logger {
|
|
l := ctx.Logger.With().Str(logOperation, operation)
|
|
if params != nil {
|
|
l = params(l)
|
|
}
|
|
return log.From(l)
|
|
}
|
|
|
|
func (j *Client) maxCallsCheck(calls int, ctx Context) Error {
|
|
if calls > ctx.Session.Capabilities.Core.MaxCallsInRequest {
|
|
ctx.Logger.Error().
|
|
Int("max-calls-in-request", ctx.Session.Capabilities.Core.MaxCallsInRequest).
|
|
Int("calls-in-request", calls).
|
|
Msgf("number of calls in request payload (%d) exceeds the allowed maximum (%d)", ctx.Session.Capabilities.Core.MaxCallsInRequest, calls)
|
|
return jmapError(errTooManyMethodCalls, JmapErrorTooManyMethodCalls)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Construct a Request from the given list of Invocation objects.
|
|
//
|
|
// If an issue occurs, then it is logged prior to returning it.
|
|
func (j *Client) request(ctx Context, using []JmapNamespace, methodCalls ...Invocation) (Request, Error) {
|
|
sanitized := structs.Filter(methodCalls, func(inv Invocation) bool { return inv.Command != "" })
|
|
|
|
err := j.maxCallsCheck(len(sanitized), ctx)
|
|
if err != nil {
|
|
return Request{}, err
|
|
}
|
|
|
|
if using == nil {
|
|
using = JmapNamespaces
|
|
}
|
|
if !slices.Contains(using, JmapCore) {
|
|
using = slices.Insert(using, 0, JmapCore)
|
|
}
|
|
return Request{
|
|
Using: using,
|
|
MethodCalls: sanitized,
|
|
CreatedIds: nil,
|
|
}, nil
|
|
}
|