Files
opencloud/pkg/jmap/jmap_session.go
Pascal Bleser 8d9c3b0c4e Groupware improvements
* ensure that all the jmap responses contain the SessionState

 * implement missing errors that were marked as TODO

 * moved common functions from pkg/jmap and pkg/services/groupware to
   pkg/log and pkg/structs to commonalize them across both source trees

 * implement error handling for SetError occurences

 * Email: replace anonymous map[string]bool for mailbox rights with a
   MailboxRights struct, as the keys are well-defined, which allows for
   properly documenting them

 * introduce ObjectType as an "enum"

 * fix JSON marshalling and unmarshalling of EmailBodyStructure

 * move the swagger documentation structs from groupware_api.go to
   groupware_docs.go

 * fix: change verb for /groupware/accounts/*/vacation from POST to PUT
2025-12-09 09:15:36 +01:00

120 lines
4.4 KiB
Go

package jmap
import (
"fmt"
"net/url"
"github.com/opencloud-eu/opencloud/pkg/log"
)
type SessionEventListener interface {
OnSessionOutdated(session *Session)
}
// Cached user related information
//
// This information is typically retrieved once (or at least for a certain period of time) from the
// JMAP well-known endpoint of Stalwart and then kept in cache to avoid the performance cost of
// retrieving it over and over again.
//
// This is really only needed due to the Graph API limitations, since ideally, the account ID should
// be passed as a request parameter by the UI, in order to support a user having multiple accounts.
//
// Keeping track of the JMAP URL might be useful though, in case of Stalwart sharding strategies making
// use of that, by providing different URLs for JMAP on a per-user basis, and that is not something
// we would want to query before every single JMAP request. On the other hand, that then also creates
// a risk of going out-of-sync, e.g. if a node is down and the user is reassigned to a different node.
// There might be webhooks to subscribe to in Stalwart to be notified of such situations, in which case
// the Session needs to be removed from the cache.
//
// The Username is only here for convenience, it could just as well be passed as a separate parameter
// instead of being part of the Session, since the username is always part of the request (typically in
// the authentication token payload.)
type Session struct {
// The name of the user to use to authenticate against Stalwart
Username string
// The base URL to use for JMAP operations towards Stalwart
JmapUrl url.URL
// The upload URL template
UploadUrlTemplate string
// The upload URL template
DownloadUrlTemplate string
// TODO
DefaultMailAccountId string
SessionResponse
}
// Create a new Session from a SessionResponse.
func newSession(sessionResponse SessionResponse) (Session, Error) {
username := sessionResponse.Username
if username == "" {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide a username")}
}
mailAccountId := sessionResponse.PrimaryAccounts.Mail
if mailAccountId == "" {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide a primary mail account")}
}
apiStr := sessionResponse.ApiUrl
if apiStr == "" {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide an API URL")}
}
apiUrl, err := url.Parse(apiStr)
if err != nil {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response provides an invalid API URL")}
}
uploadUrl := sessionResponse.UploadUrl
if uploadUrl == "" {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide an upload URL")}
}
downloadUrl := sessionResponse.DownloadUrl
if downloadUrl == "" {
return Session{}, SimpleError{code: JmapErrorInvalidSessionResponse, err: fmt.Errorf("JMAP session response does not provide an download URL")}
}
return Session{
Username: username,
DefaultMailAccountId: mailAccountId,
JmapUrl: *apiUrl,
UploadUrlTemplate: uploadUrl,
DownloadUrlTemplate: downloadUrl,
SessionResponse: sessionResponse,
}, nil
}
func (s *Session) MailAccountId(accountId string) string {
if accountId != "" && accountId != defaultAccountId {
return accountId
}
// TODO(pbleser-oc) handle case where there is no default mail account
return s.DefaultMailAccountId
}
func (s *Session) BlobAccountId(accountId string) string {
if accountId != "" && accountId != defaultAccountId {
return accountId
}
// TODO(pbleser-oc) handle case where there is no default blob account
return s.PrimaryAccounts.Blob
}
func (s *Session) SubmissionAccountId(accountId string) string {
if accountId != "" && accountId != defaultAccountId {
return accountId
}
// TODO(pbleser-oc) handle case where there is no default submission account
return s.PrimaryAccounts.Submission
}
// Create a new log.Logger that is decorated with fields containing information about the Session.
func (s Session) DecorateLogger(l log.Logger) *log.Logger {
return log.From(l.With().
Str(logUsername, s.Username).
Str(logApiUrl, s.ApiUrl).
Str(logSessionState, s.State))
}