mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-06 12:21:21 -05:00
groupware: session handling improvements
* remove the baseurl from the JMAP client configuration, and pass it to the session retrieval functions instead, as that is really the only place where it is relevant, and we gain flexibility to discover that session URL differently in the future without having to touch the JMAP client * move the default account identifier handling from the JMAP package to the Groupware one, as it really has nothing to do with JMAP itself, and is an opinionated feature of the Groupware REST API instead * add an event listener interface for JMAP events to be more flexible and universal, typically for metrics that are defined on the API level that uses the JMAP client * add errors for when default accounts cannot be determined * split groupware_framework.go into groupware_framework.go, groupware_request.go and groupware_response.go * move the accountId logging into the Groupware level instead of JMAP since it can also be relevant to other operations that might be worthy of logging before the JMAP client is even invoked
This commit is contained in:
@@ -3,6 +3,7 @@ package jmap
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
@@ -13,29 +14,24 @@ type ApiClient interface {
|
||||
}
|
||||
|
||||
type SessionClient interface {
|
||||
GetSession(username string, logger *log.Logger) (SessionResponse, Error)
|
||||
GetSession(baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
|
||||
}
|
||||
|
||||
type BlobClient interface {
|
||||
UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, contentType string, content io.Reader) (UploadedBlob, Error)
|
||||
DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string) (*BlobDownload, Error)
|
||||
UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, content io.Reader) (UploadedBlob, Error)
|
||||
DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error)
|
||||
}
|
||||
|
||||
const (
|
||||
logHttpStatusCode = "status"
|
||||
logOperation = "operation"
|
||||
logUsername = "username"
|
||||
logAccountId = "account-id"
|
||||
logMailboxId = "mailbox-id"
|
||||
logFetchBodies = "fetch-bodies"
|
||||
logOffset = "offset"
|
||||
logLimit = "limit"
|
||||
logEndpoint = "endpoint"
|
||||
logDownloadUrl = "downloadurl"
|
||||
logBlobId = "blobId"
|
||||
logUploadUrl = "downloadurl"
|
||||
logSessionState = "session-state"
|
||||
logSince = "since"
|
||||
|
||||
defaultAccountId = "*"
|
||||
logOperation = "operation"
|
||||
logUsername = "username"
|
||||
logMailboxId = "mailbox-id"
|
||||
logFetchBodies = "fetch-bodies"
|
||||
logOffset = "offset"
|
||||
logLimit = "limit"
|
||||
logDownloadUrl = "download-url"
|
||||
logBlobId = "blob-id"
|
||||
logUploadUrl = "download-url"
|
||||
logSessionState = "session-state"
|
||||
logSince = "since"
|
||||
)
|
||||
|
||||
@@ -15,11 +15,9 @@ type BlobResponse struct {
|
||||
}
|
||||
|
||||
func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, id string) (BlobResponse, SessionState, Error) {
|
||||
aid := session.BlobAccountId(accountId)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandBlobUpload, BlobGetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Ids: []string{id},
|
||||
Properties: []string{BlobPropertyData, BlobPropertyDigestSha512, BlobPropertySize},
|
||||
}, "0"),
|
||||
@@ -56,32 +54,28 @@ type UploadedBlob struct {
|
||||
|
||||
func (j *Client) UploadBlobStream(accountId string, session *Session, ctx context.Context, logger *log.Logger, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
logger = log.From(logger.With().Str(logEndpoint, session.UploadEndpoint))
|
||||
aid := session.BlobAccountId(accountId)
|
||||
// TODO(pbleser-oc) use a library for proper URL template parsing
|
||||
uploadUrl := strings.ReplaceAll(session.UploadUrlTemplate, "{accountId}", aid)
|
||||
return j.blob.UploadBinary(ctx, logger, session, uploadUrl, contentType, body)
|
||||
uploadUrl := strings.ReplaceAll(session.UploadUrlTemplate, "{accountId}", accountId)
|
||||
return j.blob.UploadBinary(ctx, logger, session, uploadUrl, session.UploadEndpoint, contentType, body)
|
||||
}
|
||||
|
||||
func (j *Client) DownloadBlobStream(accountId string, blobId string, name string, typ string, session *Session, ctx context.Context, logger *log.Logger) (*BlobDownload, Error) {
|
||||
logger = log.From(logger.With().Str(logEndpoint, session.DownloadEndpoint))
|
||||
aid := session.BlobAccountId(accountId)
|
||||
// TODO(pbleser-oc) use a library for proper URL template parsing
|
||||
downloadUrl := session.DownloadUrlTemplate
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{accountId}", aid)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{accountId}", accountId)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{blobId}", blobId)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{name}", name)
|
||||
downloadUrl = strings.ReplaceAll(downloadUrl, "{type}", typ)
|
||||
logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId).Str(logAccountId, aid))
|
||||
return j.blob.DownloadBinary(ctx, logger, session, downloadUrl)
|
||||
logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId))
|
||||
return j.blob.DownloadBinary(ctx, logger, session, downloadUrl, session.DownloadEndpoint)
|
||||
}
|
||||
|
||||
func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte, contentType string) (UploadedBlob, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
upload := BlobUploadCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Create: map[string]UploadObject{
|
||||
"0": {
|
||||
Data: []DataSourceObject{{
|
||||
@@ -93,7 +87,7 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
|
||||
}
|
||||
|
||||
getHash := BlobGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandBlobUpload,
|
||||
|
||||
@@ -31,10 +31,9 @@ type Emails struct {
|
||||
}
|
||||
|
||||
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetEmails", session, logger)
|
||||
logger = j.logger(accountId, "GetEmails", session, logger)
|
||||
|
||||
get := EmailGetCommand{AccountId: aid, Ids: ids, FetchAllBodyValues: fetchBodies}
|
||||
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
|
||||
if maxBodyValueBytes > 0 {
|
||||
get.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
@@ -56,13 +55,12 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
|
||||
}
|
||||
|
||||
func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "GetAllEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "GetAllEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Uint(logOffset, offset).Uint(logLimit, limit)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: &EmailFilterCondition{InMailbox: mailboxId},
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
@@ -76,7 +74,7 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
}
|
||||
|
||||
get := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"},
|
||||
}
|
||||
@@ -127,13 +125,12 @@ type EmailsSince struct {
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, since string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailsSince, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "GetEmailsInMailboxSince", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "GetEmailsInMailboxSince", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
|
||||
})
|
||||
|
||||
changes := MailboxChangesCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
SinceState: since,
|
||||
}
|
||||
if maxChanges > 0 {
|
||||
@@ -141,7 +138,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
}
|
||||
|
||||
getCreated := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
|
||||
}
|
||||
@@ -149,7 +146,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
getCreated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
getUpdated := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
|
||||
}
|
||||
@@ -201,13 +198,12 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, since string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailsSince, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
|
||||
})
|
||||
|
||||
changes := EmailChangesCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
SinceState: since,
|
||||
}
|
||||
if maxChanges > 0 {
|
||||
@@ -215,7 +211,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
}
|
||||
|
||||
getCreated := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/created", ResultOf: "0"},
|
||||
}
|
||||
@@ -223,7 +219,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
getCreated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
getUpdated := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/updated", ResultOf: "0"},
|
||||
}
|
||||
@@ -282,13 +278,12 @@ type EmailSnippetQueryResult struct {
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint) (EmailSnippetQueryResult, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Uint(logLimit, limit).Uint(logOffset, offset)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
@@ -302,7 +297,7 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement,
|
||||
}
|
||||
|
||||
snippet := SearchSnippetGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
EmailIdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
@@ -356,13 +351,12 @@ type EmailQueryResult struct {
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryResult, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
@@ -376,7 +370,7 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
}
|
||||
|
||||
mails := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandEmailQuery,
|
||||
@@ -434,13 +428,12 @@ type EmailQueryWithSnippetsResult struct {
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryWithSnippetsResult, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
@@ -454,7 +447,7 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
|
||||
}
|
||||
|
||||
snippet := SearchSnippetGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
EmailIdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
@@ -464,7 +457,7 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
|
||||
}
|
||||
|
||||
mails := EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandEmailQuery,
|
||||
@@ -544,12 +537,10 @@ type UploadedEmail struct {
|
||||
}
|
||||
|
||||
func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (UploadedEmail, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
upload := BlobUploadCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Create: map[string]UploadObject{
|
||||
"0": {
|
||||
Data: []DataSourceObject{{
|
||||
@@ -561,7 +552,7 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
|
||||
}
|
||||
|
||||
getHash := BlobGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandBlobUpload,
|
||||
@@ -625,11 +616,9 @@ type CreatedEmail struct {
|
||||
}
|
||||
|
||||
func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger) (CreatedEmail, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandEmailSubmissionSet, EmailSetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Create: map[string]EmailCreate{
|
||||
"c": email,
|
||||
},
|
||||
@@ -687,11 +676,9 @@ type UpdatedEmails struct {
|
||||
//
|
||||
// To delete mails, use the DeleteEmails function instead.
|
||||
func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, session *Session, ctx context.Context, logger *log.Logger) (UpdatedEmails, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandEmailSet, EmailSetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Update: updates,
|
||||
}, "0"),
|
||||
)
|
||||
@@ -723,11 +710,9 @@ type DeletedEmails struct {
|
||||
}
|
||||
|
||||
func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger) (DeletedEmails, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandEmailSet, EmailSetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Destroy: destroy,
|
||||
}, "0"),
|
||||
)
|
||||
@@ -777,10 +762,8 @@ type SubmittedEmail struct {
|
||||
}
|
||||
|
||||
func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (SubmittedEmail, SessionState, Error) {
|
||||
aid := session.SubmissionAccountId(accountId)
|
||||
|
||||
set := EmailSubmissionSetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Create: map[string]EmailSubmissionCreate{
|
||||
"s0": {
|
||||
IdentityId: identityId,
|
||||
@@ -795,7 +778,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
}
|
||||
|
||||
get := EmailSubmissionGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandEmailSubmissionSet,
|
||||
@@ -865,18 +848,17 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
}
|
||||
|
||||
func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.loggerParams(aid, "EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
logger = j.loggerParams(accountId, "EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
|
||||
})
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandThreadGet, ThreadGetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Ids: []string{threadId},
|
||||
}, "0"),
|
||||
invocation(CommandEmailGet, EmailGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandThreadGet,
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Identities struct {
|
||||
@@ -16,9 +15,8 @@ type Identities struct {
|
||||
|
||||
// https://jmap.io/spec-mail.html#identityget
|
||||
func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (Identities, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetIdentity", session, logger)
|
||||
cmd, err := request(invocation(CommandIdentityGet, IdentityGetCommand{AccountId: aid}, "0"))
|
||||
logger = j.logger(accountId, "GetIdentity", session, logger)
|
||||
cmd, err := request(invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, "0"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return Identities{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
@@ -46,9 +44,7 @@ type IdentitiesGetResponse struct {
|
||||
func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, SessionState, Error) {
|
||||
uniqueAccountIds := structs.Uniq(accountIds)
|
||||
|
||||
logger = j.loggerParams("", "GetIdentities", session, logger, func(l zerolog.Context) zerolog.Context {
|
||||
return l.Array(logAccountId, log.SafeStringArray(uniqueAccountIds))
|
||||
})
|
||||
logger = j.logger("", "GetIdentities", session, logger)
|
||||
|
||||
calls := make([]Invocation, len(uniqueAccountIds))
|
||||
for i, accountId := range uniqueAccountIds {
|
||||
@@ -95,9 +91,7 @@ type IdentitiesAndMailboxesGetResponse struct {
|
||||
func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesAndMailboxesGetResponse, SessionState, Error) {
|
||||
uniqueAccountIds := structs.Uniq(accountIds)
|
||||
|
||||
logger = j.loggerParams("", "GetIdentitiesAndMailboxes", session, logger, func(l zerolog.Context) zerolog.Context {
|
||||
return l.Array(logAccountId, log.SafeStringArray(uniqueAccountIds))
|
||||
})
|
||||
logger = j.logger("", "GetIdentitiesAndMailboxes", session, logger)
|
||||
|
||||
calls := make([]Invocation, len(uniqueAccountIds)+1)
|
||||
calls[0] = invocation(CommandMailboxGet, MailboxGetCommand{AccountId: mailboxAccountId}, "0")
|
||||
|
||||
@@ -14,9 +14,8 @@ type MailboxesResponse struct {
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxget
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetMailbox", session, logger)
|
||||
cmd, err := request(invocation(CommandMailboxGet, MailboxGetCommand{AccountId: aid, Ids: ids}, "0"))
|
||||
logger = j.logger(accountId, "GetMailbox", session, logger)
|
||||
cmd, err := request(invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, "0"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return MailboxesResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
@@ -62,13 +61,12 @@ type Mailboxes struct {
|
||||
}
|
||||
|
||||
func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (Mailboxes, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "SearchMailboxes", session, logger)
|
||||
logger = j.logger(accountId, "SearchMailboxes", session, logger)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: aid, Filter: filter}, "0"),
|
||||
invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: filter}, "0"),
|
||||
invocation(CommandMailboxGet, MailboxGetRefCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
IdRef: &ResultReference{Name: CommandMailboxQuery, Path: "/ids/*", ResultOf: "0"},
|
||||
}, "1"),
|
||||
)
|
||||
|
||||
@@ -14,9 +14,8 @@ const (
|
||||
|
||||
// https://jmap.io/spec-mail.html#vacationresponseget
|
||||
func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "GetVacationResponse", session, logger)
|
||||
cmd, err := request(invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: aid}, "0"))
|
||||
logger = j.logger(accountId, "GetVacationResponse", session, logger)
|
||||
cmd, err := request(invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "0"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return VacationResponseGetResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
@@ -62,12 +61,11 @@ type VacationResponseChange struct {
|
||||
}
|
||||
|
||||
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, SessionState, Error) {
|
||||
aid := session.MailAccountId(accountId)
|
||||
logger = j.logger(aid, "SetVacationResponse", session, logger)
|
||||
logger = j.logger(accountId, "SetVacationResponse", session, logger)
|
||||
|
||||
cmd, err := request(
|
||||
invocation(CommandVacationResponseSet, VacationResponseSetCommand{
|
||||
AccountId: aid,
|
||||
AccountId: accountId,
|
||||
Create: map[string]VacationResponse{
|
||||
vacationResponseId: {
|
||||
IsEnabled: vacation.IsEnabled,
|
||||
@@ -81,7 +79,7 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse
|
||||
}, "0"),
|
||||
// chain a second request to get the current complete VacationResponse object
|
||||
// after performing the changes, as that makes for a better API
|
||||
invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: aid}, "1"),
|
||||
invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package jmap
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -39,8 +40,8 @@ func (j *Client) onSessionOutdated(session *Session, newSessionState SessionStat
|
||||
}
|
||||
|
||||
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
|
||||
func (j *Client) FetchSession(username string, logger *log.Logger) (Session, Error) {
|
||||
wk, err := j.session.GetSession(username, logger)
|
||||
func (j *Client) FetchSession(sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
|
||||
wk, err := j.session.GetSession(sessionUrl, username, logger)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
@@ -48,18 +49,14 @@ func (j *Client) FetchSession(username string, logger *log.Logger) (Session, Err
|
||||
}
|
||||
|
||||
func (j *Client) logger(accountId string, operation string, _ *Session, logger *log.Logger) *log.Logger {
|
||||
var _ string = accountId
|
||||
l := logger.With().Str(logOperation, operation)
|
||||
if accountId != "" {
|
||||
l = l.Str(logAccountId, accountId)
|
||||
}
|
||||
return log.From(l)
|
||||
}
|
||||
|
||||
func (j *Client) loggerParams(accountId string, operation string, _ *Session, logger *log.Logger, params func(zerolog.Context) zerolog.Context) *log.Logger {
|
||||
var _ string = accountId
|
||||
l := logger.With().Str(logOperation, operation)
|
||||
if accountId != "" {
|
||||
l = l.Str(logAccountId, accountId)
|
||||
}
|
||||
if params != nil {
|
||||
l = params(l)
|
||||
}
|
||||
|
||||
@@ -4,31 +4,38 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
)
|
||||
|
||||
type HttpJmapApiClient struct {
|
||||
baseurl url.URL
|
||||
// Implementation of ApiClient, SessionClient and BlobClient that uses
|
||||
// HTTP to perform JMAP operations.
|
||||
type HttpJmapClient struct {
|
||||
client *http.Client
|
||||
masterUser string
|
||||
masterPassword string
|
||||
userAgent string
|
||||
metrics HttpJmapApiClientMetrics
|
||||
listener HttpJmapApiClientEventListener
|
||||
}
|
||||
|
||||
var (
|
||||
_ ApiClient = &HttpJmapApiClient{}
|
||||
_ SessionClient = &HttpJmapApiClient{}
|
||||
_ BlobClient = &HttpJmapApiClient{}
|
||||
_ ApiClient = &HttpJmapClient{}
|
||||
_ SessionClient = &HttpJmapClient{}
|
||||
_ BlobClient = &HttpJmapClient{}
|
||||
)
|
||||
|
||||
const (
|
||||
logEndpoint = "endpoint"
|
||||
logHttpStatus = "status"
|
||||
logHttpStatusCode = "status-code"
|
||||
logHttpUrl = "url"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -37,24 +44,47 @@ func bearer(req *http.Request, token string) {
|
||||
}
|
||||
*/
|
||||
|
||||
type HttpJmapApiClientMetrics struct {
|
||||
SuccessfulRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestStatusPerEndpointCounter *prometheus.CounterVec
|
||||
// Record JMAP HTTP execution events that may occur, e.g. using metrics.
|
||||
type HttpJmapApiClientEventListener interface {
|
||||
OnSuccessfulRequest(endpoint string, status int)
|
||||
OnFailedRequest(endpoint string, err error)
|
||||
OnFailedRequestWithStatus(endpoint string, status int)
|
||||
OnResponseBodyReadingError(endpoint string, err error)
|
||||
OnResponseBodyUnmarshallingError(endpoint string, err error)
|
||||
}
|
||||
|
||||
func NewHttpJmapApiClient(baseurl url.URL, client *http.Client, masterUser string, masterPassword string, metrics HttpJmapApiClientMetrics) *HttpJmapApiClient {
|
||||
return &HttpJmapApiClient{
|
||||
baseurl: baseurl,
|
||||
type nullHttpJmapApiClientEventListener struct {
|
||||
}
|
||||
|
||||
func (l nullHttpJmapApiClientEventListener) OnSuccessfulRequest(endpoint string, status int) {
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequest(endpoint string, err error) {
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnFailedRequestWithStatus(endpoint string, status int) {
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyReadingError(endpoint string, err error) {
|
||||
}
|
||||
func (l nullHttpJmapApiClientEventListener) OnResponseBodyUnmarshallingError(endpoint string, err error) {
|
||||
}
|
||||
|
||||
var _ HttpJmapApiClientEventListener = nullHttpJmapApiClientEventListener{}
|
||||
|
||||
// An implementation of HttpJmapApiClientMetricsRecorder that does nothing.
|
||||
func NullHttpJmapApiClientEventListener() HttpJmapApiClientEventListener {
|
||||
return nullHttpJmapApiClientEventListener{}
|
||||
}
|
||||
|
||||
func NewHttpJmapClient(client *http.Client, masterUser string, masterPassword string, listener HttpJmapApiClientEventListener) *HttpJmapClient {
|
||||
return &HttpJmapClient{
|
||||
client: client,
|
||||
masterUser: masterUser,
|
||||
masterPassword: masterPassword,
|
||||
userAgent: "OpenCloud/" + version.GetString(),
|
||||
metrics: metrics,
|
||||
listener: listener,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) Close() error {
|
||||
func (h *HttpJmapClient) Close() error {
|
||||
h.client.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
@@ -70,19 +100,33 @@ func (e AuthenticationError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) auth(username string, _ *log.Logger, req *http.Request) error {
|
||||
func (h *HttpJmapClient) auth(username string, _ *log.Logger, req *http.Request) error {
|
||||
masterUsername := username + "%" + h.masterUser
|
||||
req.SetBasicAuth(masterUsername, h.masterPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
sessionUrl := h.baseurl.JoinPath(".well-known", "jmap")
|
||||
var (
|
||||
errNilBaseUrl = errors.New("sessionUrl is nil")
|
||||
)
|
||||
|
||||
func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
if sessionUrl == nil {
|
||||
logger.Error().Msg("sessionUrl is nil")
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: errNilBaseUrl}
|
||||
}
|
||||
// See the JMAP specification on Service Autodiscovery: https://jmap.io/spec-core.html#service-autodiscovery
|
||||
// There are two standardised autodiscovery methods in use for Internet protocols:
|
||||
// - DNS SRV (see [@!RFC2782], [@!RFC6186], and [@!RFC6764])
|
||||
// - .well-known/servicename (see [@!RFC8615])
|
||||
// We are currently only supporting RFC8615, using the baseurl that was configured in this HttpJmapApiClient.
|
||||
//sessionUrl := baseurl.JoinPath(".well-known", "jmap")
|
||||
sessionUrlStr := sessionUrl.String()
|
||||
endpoint := endpointOf(sessionUrl)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, sessionUrlStr, nil)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create GET request for %v", sessionUrl)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to create GET request for %v", sessionUrl)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
h.auth(username, logger, req)
|
||||
@@ -90,22 +134,16 @@ func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (Ses
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl)).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", sessionUrl)
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to perform GET %v", sessionUrl)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorInvalidHttpRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl), strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str(logHttpStatusCode, res.Status).Msg("HTTP response status code is not 200")
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(endpointOf(sessionUrl)).Inc()
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
|
||||
if res.Body != nil {
|
||||
defer func(Body io.ReadCloser) {
|
||||
@@ -118,32 +156,35 @@ func (h *HttpJmapApiClient) GetSession(username string, logger *log.Logger) (Ses
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorReadingResponseBody, err: err}
|
||||
}
|
||||
|
||||
var data SessionResponse
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
logger.Error().Str("url", sessionUrlStr).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpUrl, sessionUrlStr).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
|
||||
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
|
||||
func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) {
|
||||
jmapUrl := session.JmapUrl.String()
|
||||
endpoint := session.JmapEndpoint
|
||||
|
||||
bodyBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to marshall JSON payload")
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msg("failed to marshall JSON payload")
|
||||
return nil, SimpleError{code: JmapErrorEncodingRequestBody, err: err}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to create POST request for %v", jmapUrl)
|
||||
return nil, SimpleError{code: JmapErrorCreatingRequest, err: err}
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
@@ -152,17 +193,13 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.JmapEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to perform POST %v", jmapUrl)
|
||||
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.JmapEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Msg("HTTP response status code is not 2xx")
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
if res.Body != nil {
|
||||
@@ -173,23 +210,22 @@ func (h *HttpJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses
|
||||
}
|
||||
}(res.Body)
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.JmapEndpoint).Inc()
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create POST request for %v", uploadUrl)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to create POST request for %v", uploadUrl)
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorCreatingRequest, err: err}
|
||||
}
|
||||
req.Header.Add("Content-Type", contentType)
|
||||
@@ -198,17 +234,13 @@ func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.UploadEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl)
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to perform POST %v", uploadUrl)
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.UploadEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
if res.Body != nil {
|
||||
@@ -219,30 +251,30 @@ func (h *HttpJmapApiClient) UploadBinary(ctx context.Context, logger *log.Logger
|
||||
}
|
||||
}(res.Body)
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.UploadEndpoint).Inc()
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
|
||||
responseBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to read response body")
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msg("failed to read response body")
|
||||
h.listener.OnResponseBodyReadingError(endpoint, err)
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
|
||||
var result UploadedBlob
|
||||
err = json.Unmarshal(responseBody, &result)
|
||||
if err != nil {
|
||||
logger.Error().Str("url", uploadUrl).Err(err).Msg("failed to decode JSON payload from the upload response")
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpUrl, uploadUrl).Err(err).Msg("failed to decode JSON payload from the upload response")
|
||||
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
|
||||
return UploadedBlob{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *HttpJmapApiClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string) (*BlobDownload, Error) {
|
||||
func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("failed to create GET request for %v", downloadUrl)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to create GET request for %v", downloadUrl)
|
||||
return nil, SimpleError{code: JmapErrorCreatingRequest, err: err}
|
||||
}
|
||||
req.Header.Add("User-Agent", h.userAgent)
|
||||
@@ -250,32 +282,26 @@ func (h *HttpJmapApiClient) DownloadBinary(ctx context.Context, logger *log.Logg
|
||||
|
||||
res, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
if h.metrics.FailedRequestPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestPerEndpointCounter.WithLabelValues(session.DownloadEndpoint).Inc()
|
||||
}
|
||||
logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl)
|
||||
h.listener.OnFailedRequest(endpoint, err)
|
||||
logger.Error().Err(err).Str(logEndpoint, endpoint).Msgf("failed to perform GET %v", downloadUrl)
|
||||
return nil, SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
if h.metrics.FailedRequestStatusPerEndpointCounter != nil {
|
||||
h.metrics.FailedRequestStatusPerEndpointCounter.WithLabelValues(session.DownloadEndpoint, strconv.Itoa(res.StatusCode)).Inc()
|
||||
}
|
||||
logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 2xx")
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
|
||||
return nil, SimpleError{code: JmapErrorServerResponse, err: err}
|
||||
}
|
||||
if h.metrics.SuccessfulRequestPerEndpointCounter != nil {
|
||||
h.metrics.SuccessfulRequestPerEndpointCounter.WithLabelValues(session.DownloadEndpoint).Inc()
|
||||
}
|
||||
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
|
||||
|
||||
sizeStr := res.Header.Get("Content-Length")
|
||||
size := -1
|
||||
if sizeStr != "" {
|
||||
size, err = strconv.Atoi(sizeStr)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("failed to parse Content-Length blob download response header value '%v'", sizeStr)
|
||||
logger.Warn().Err(err).Str(logEndpoint, endpoint).Msgf("failed to parse Content-Length blob download response header value '%v'", sizeStr)
|
||||
size = -1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,30 +101,6 @@ func newSession(sessionResponse SessionResponse) (Session, Error) {
|
||||
}, 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.PrimaryAccounts.Mail
|
||||
}
|
||||
|
||||
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().
|
||||
|
||||
@@ -32,7 +32,7 @@ func (t *TestJmapWellKnownClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestJmapWellKnownClient) GetSession(username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
func (t *TestJmapWellKnownClient) GetSession(sessionUrl *url.URL, username string, logger *log.Logger) (SessionResponse, Error) {
|
||||
pa := generateRandomString(2 + seededRand.Intn(10))
|
||||
return SessionResponse{
|
||||
Username: generateRandomString(8),
|
||||
@@ -70,7 +70,7 @@ func NewTestJmapBlobClient(t *testing.T) BlobClient {
|
||||
return &TestJmapBlobClient{t: t}
|
||||
}
|
||||
|
||||
func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
bytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return UploadedBlob{}, SimpleError{code: 0, err: err}
|
||||
@@ -85,7 +85,7 @@ func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string) (*BlobDownload, Error) {
|
||||
func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error) {
|
||||
return &BlobDownload{
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Size: -1,
|
||||
|
||||
@@ -46,8 +46,8 @@ func DefaultConfig() *config.Config {
|
||||
Namespace: "eu.opencloud.web",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Cache-Control"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "REPORT"},
|
||||
AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Trace-Id", "Cache-Control"},
|
||||
AllowCredentials: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
account, err := req.GetAccount()
|
||||
account, err := req.GetAccountForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
@@ -56,10 +57,14 @@ type SwaggerAccountBootstrapResponse struct {
|
||||
|
||||
func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
mailAccountId := req.GetAccountId()
|
||||
mailAccountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, mailAccountId))
|
||||
accountIds := structs.Keys(req.session.Accounts)
|
||||
|
||||
resp, sessionState, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, req.logger)
|
||||
resp, sessionState, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,9 +22,15 @@ func (g *Groupware) GetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
return req.parameterErrorResponse(UriParamBlobId, fmt.Sprintf("Invalid value for path parameter '%v': empty", UriParamBlobId))
|
||||
}
|
||||
|
||||
res, _, err := g.jmap.GetBlob(req.GetAccountId(), req.session, req.ctx, req.logger, blobId)
|
||||
accountId, err := req.GetAccountIdForBlob()
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
res, _, jerr := g.jmap.GetBlob(accountId, req.session, req.ctx, logger, blobId)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
blob := res.Blob
|
||||
if blob == nil {
|
||||
@@ -46,9 +53,15 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
}(body)
|
||||
}
|
||||
|
||||
resp, err := g.jmap.UploadBlobStream(req.GetAccountId(), req.session, req.ctx, req.logger, contentType, body)
|
||||
accountId, err := req.GetAccountIdForBlob()
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
resp, jerr := g.jmap.UploadBlobStream(accountId, req.session, req.ctx, logger, contentType, body)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return etagOnlyResponse(resp, jmap.State(resp.Sha512))
|
||||
@@ -60,17 +73,23 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
blobId := chi.URLParam(req.r, UriParamBlobId)
|
||||
name := chi.URLParam(req.r, UriParamBlobName)
|
||||
q := req.r.URL.Query()
|
||||
tipe := q.Get(QueryParamBlobType)
|
||||
if tipe == "" {
|
||||
tipe = DefaultBlobDownloadType
|
||||
typ := q.Get(QueryParamBlobType)
|
||||
if typ == "" {
|
||||
typ = DefaultBlobDownloadType
|
||||
}
|
||||
|
||||
blob, jerr := g.jmap.DownloadBlobStream(req.GetAccountId(), blobId, name, tipe, req.session, req.ctx, req.logger)
|
||||
accountId, gwerr := req.GetAccountIdForBlob()
|
||||
if gwerr != nil {
|
||||
return gwerr
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
blob, jerr := g.jmap.DownloadBlobStream(accountId, blobId, name, typ, req.session, req.ctx, logger)
|
||||
if blob != nil && blob.Body != nil {
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
req.logger.Error().Err(err).Msg("failed to close response body")
|
||||
logger.Error().Err(err).Msg("failed to close response body")
|
||||
}
|
||||
}(blob.Body)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
// When the request suceeds.
|
||||
@@ -26,9 +27,14 @@ type SwaggerGetIdentitiesResponse struct {
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, sessionState, err := g.jmap.GetIdentity(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
accountId, err := req.GetAccountIdWithoutFallback()
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
res, sessionState, jerr := g.jmap.GetIdentity(accountId, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
return etagResponse(res, sessionState, res.State)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
// When the request succeeds.
|
||||
@@ -33,9 +34,14 @@ type SwaggerGetMailboxById200 struct {
|
||||
func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, UriParamMailboxId)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, sessionState, err := g.jmap.GetMailbox(req.GetAccountId(), req.session, req.ctx, req.logger, []string{mailboxId})
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
res, sessionState, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, []string{mailboxId})
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
if len(res.Mailboxes) == 1 {
|
||||
@@ -107,14 +113,20 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
hasCriteria = true
|
||||
}
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
if hasCriteria {
|
||||
mailboxes, sessionState, err := g.jmap.SearchMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger, filter)
|
||||
mailboxes, sessionState, err := g.jmap.SearchMailboxes(accountId, req.session, req.ctx, logger, filter)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
|
||||
} else {
|
||||
mailboxes, sessionState, err := g.jmap.GetAllMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
mailboxes, sessionState, err := g.jmap.GetAllMailboxes(accountId, req.session, req.ctx, logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
|
||||
@@ -64,9 +64,15 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
|
||||
if mailboxId == "" {
|
||||
return req.parameterErrorResponse(UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(HeaderSince, since))
|
||||
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsInMailboxSince(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str(HeaderSince, since).Str(logAccountId, accountId))
|
||||
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsInMailboxSince(accountId, req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -95,9 +101,15 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
emails, sessionState, jerr := g.jmap.GetAllEmails(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
emails, sessionState, jerr := g.jmap.GetAllEmails(accountId, req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -115,8 +127,14 @@ func (g *Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) {
|
||||
return req.parameterErrorResponse(UriParamMessageId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamMessageId, log.SafeString(id), "empty list of mail ids"))
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str("id", log.SafeString(id)))
|
||||
emails, sessionState, jerr := g.jmap.GetEmails(req.GetAccountId(), req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes)
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str("id", log.SafeString(id)).Str(logAccountId, log.SafeString(accountId)))
|
||||
|
||||
emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -135,9 +153,16 @@ func (g *Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, sin
|
||||
if ok {
|
||||
l = l.Uint(QueryParamMaxChanges, maxChanges)
|
||||
}
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
l = l.Str(logAccountId, log.SafeString(accountId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsSince(accountId, req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -328,7 +353,13 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
logger = log.From(logger.With().Bool(QueryParamSearchFetchBodies, fetchBodies))
|
||||
}
|
||||
|
||||
results, sessionState, jerr := g.jmap.QueryEmailsWithSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes)
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger = log.From(logger.With().Str(logAccountId, accountId))
|
||||
|
||||
results, sessionState, jerr := g.jmap.QueryEmailsWithSnippets(accountId, filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -355,7 +386,13 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
QueryState: results.QueryState,
|
||||
}, sessionState, results.QueryState)
|
||||
} else {
|
||||
results, sessionState, jerr := g.jmap.QueryEmailSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit)
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger = log.From(logger.With().Str(logAccountId, accountId))
|
||||
|
||||
results, sessionState, jerr := g.jmap.QueryEmailSnippets(accountId, filter, req.session, req.ctx, logger, offset, limit)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -400,6 +437,12 @@ func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
logger := req.logger
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(gwerr)
|
||||
}
|
||||
logger = log.From(logger.With().Str(logAccountId, accountId))
|
||||
|
||||
var body MessageCreation
|
||||
err := req.body(&body)
|
||||
if err != nil {
|
||||
@@ -427,7 +470,7 @@ func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
BodyValues: body.BodyValues,
|
||||
}
|
||||
|
||||
created, sessionState, jerr := g.jmap.CreateEmail(req.GetAccountId(), create, req.session, req.ctx, logger)
|
||||
created, sessionState, jerr := g.jmap.CreateEmail(accountId, create, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -443,6 +486,12 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamMessageId, messageId)
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(gwerr)
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
var body map[string]any
|
||||
@@ -455,7 +504,7 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
messageId: body,
|
||||
}
|
||||
|
||||
result, sessionState, jerr := g.jmap.UpdateEmails(req.GetAccountId(), updates, req.session, req.ctx, logger)
|
||||
result, sessionState, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -481,9 +530,16 @@ func (g *Groupware) DeleteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamMessageId, messageId)
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(gwerr)
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
_, sessionState, jerr := g.jmap.DeleteEmails(req.GetAccountId(), []string{messageId}, req.session, req.ctx, logger)
|
||||
_, sessionState, jerr := g.jmap.DeleteEmails(accountId, []string{messageId}, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -546,19 +602,33 @@ func (g *Groupware) RelatedToMessage(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, UriParamMessageId)
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
limit, _, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit
|
||||
l := req.logger.With().Str(logEmailId, log.SafeString(id))
|
||||
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint("limit", limit)
|
||||
}
|
||||
|
||||
days, _, err := req.parseUIntParam(QueryParamDays, 5) // TODO configurable default days
|
||||
days, ok, err := req.parseUIntParam(QueryParamDays, 5) // TODO configurable default days
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint("days", days)
|
||||
}
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(gwerr)
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
reqId := req.GetRequestId()
|
||||
accountId := req.GetAccountId()
|
||||
logger := log.From(req.logger.With().Str(logEmailId, log.SafeString(id)))
|
||||
getEmailsBefore := time.Now()
|
||||
emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, []string{id}, true, g.maxBodyValueBytes)
|
||||
getEmailsDuration := time.Since(getEmailsBefore)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
// When the request succeeds.
|
||||
@@ -30,9 +31,15 @@ type SwaggerGetVacationResponse200 struct {
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, sessionState, err := g.jmap.GetVacationResponse(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
accountId, err := req.GetAccountIdForVacationResponse()
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
res, sessionState, jerr := g.jmap.GetVacationResponse(accountId, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
return etagResponse(res, sessionState, res.State)
|
||||
})
|
||||
@@ -66,7 +73,13 @@ func (g *Groupware) SetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
res, sessionState, jerr := g.jmap.SetVacationResponse(req.GetAccountId(), body, req.session, req.ctx, req.logger)
|
||||
accountId, err := req.GetAccountIdForVacationResponse()
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
res, sessionState, jerr := g.jmap.SetVacationResponse(accountId, body, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ const (
|
||||
ErrorCodeInvalidRequestParameter = "INVPAR"
|
||||
ErrorCodeInvalidRequestBody = "INVBDY"
|
||||
ErrorCodeNonExistingAccount = "INVACC"
|
||||
ErrorCodeIndeterminateAccount = "INDACC"
|
||||
ErrorCodeApiInconsistency = "APIINC"
|
||||
ErrorCodeInvalidUserRequest = "INVURQ"
|
||||
)
|
||||
@@ -275,12 +276,18 @@ var (
|
||||
Title: "Invalid Request",
|
||||
Detail: "The request is invalid.",
|
||||
}
|
||||
ErrorNonExistingAccount = GroupwareError{
|
||||
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,
|
||||
Title: "Failed to determine Account",
|
||||
Detail: "The account the request is for could not be determined.",
|
||||
}
|
||||
ErrorApiInconsistency = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeApiInconsistency,
|
||||
|
||||
@@ -5,15 +5,12 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/r3labs/sse/v2"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -29,12 +26,12 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics"
|
||||
groupwaremiddleware "github.com/opencloud-eu/opencloud/services/groupware/pkg/middleware"
|
||||
)
|
||||
|
||||
const (
|
||||
logUsername = "username" // this should match jmap.logUsername to avoid having the field twice in the logs under different keys
|
||||
logUserId = "user-id"
|
||||
logAccountId = "account-id"
|
||||
logErrorId = "error-id"
|
||||
logErrorCode = "code"
|
||||
logErrorStatus = "status"
|
||||
@@ -129,6 +126,28 @@ type Event struct {
|
||||
Body any
|
||||
}
|
||||
|
||||
type groupwareHttpJmapApiClientMetricsRecorder struct {
|
||||
m *metrics.Metrics
|
||||
}
|
||||
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnSuccessfulRequest(endpoint string, status int) {
|
||||
r.m.SuccessfulRequestPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequest(endpoint string, err error) {
|
||||
r.m.FailedRequestPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedRequestWithStatus(endpoint string, status int) {
|
||||
r.m.FailedRequestStatusPerEndpointCounter.With(metrics.EndpointAndStatus(endpoint, status)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyReadingError(endpoint string, err error) {
|
||||
r.m.ResponseBodyReadingErrorPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
func (r groupwareHttpJmapApiClientMetricsRecorder) OnResponseBodyUnmarshallingError(endpoint string, err error) {
|
||||
r.m.ResponseBodyUnmarshallingErrorPerEndpointCounter.With(metrics.Endpoint(endpoint)).Inc()
|
||||
}
|
||||
|
||||
var _ jmap.HttpJmapApiClientEventListener = groupwareHttpJmapApiClientMetricsRecorder{}
|
||||
|
||||
func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) {
|
||||
baseUrl, err := url.Parse(config.Mail.BaseUrl)
|
||||
if err != nil {
|
||||
@@ -136,6 +155,8 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
return nil, GroupwareInitializationError{Message: fmt.Sprintf("failed to parse configured Mail.BaseUrl '%s'", config.Mail.BaseUrl), Err: err}
|
||||
}
|
||||
|
||||
sessionUrl := baseUrl.JoinPath(".well-known", "jmap")
|
||||
|
||||
masterUsername := config.Mail.Master.Username
|
||||
if masterUsername == "" {
|
||||
logger.Error().Msg("failed to parse empty Mail.Master.Username")
|
||||
@@ -163,7 +184,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
insecureTls := true // TODO make configurable
|
||||
|
||||
m := metrics.New(logger)
|
||||
m := metrics.New(prometheusRegistry, logger)
|
||||
|
||||
// TODO add timeouts and other meaningful configuration settings for the HTTP client
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
@@ -177,16 +198,13 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
userProvider := NewRevaContextUsernameProvider()
|
||||
|
||||
api := jmap.NewHttpJmapApiClient(
|
||||
*baseUrl,
|
||||
jmapMetricsAdapter := groupwareHttpJmapApiClientMetricsRecorder{m: m}
|
||||
|
||||
api := jmap.NewHttpJmapClient(
|
||||
&c,
|
||||
masterUsername,
|
||||
masterPassword,
|
||||
jmap.HttpJmapApiClientMetrics{
|
||||
SuccessfulRequestPerEndpointCounter: m.SuccessfulRequestPerEndpointCounter,
|
||||
FailedRequestPerEndpointCounter: m.FailedRequestPerEndpointCounter,
|
||||
FailedRequestStatusPerEndpointCounter: m.FailedRequestStatusPerEndpointCounter,
|
||||
},
|
||||
jmapMetricsAdapter,
|
||||
)
|
||||
|
||||
jmapClient := jmap.NewClient(api, api, api)
|
||||
@@ -197,6 +215,10 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
logger: logger,
|
||||
jmapClient: &jmapClient,
|
||||
errorTtl: sessionFailureCacheTtl,
|
||||
sessionUrlProvider: func(username string) (*url.URL, *GroupwareError) {
|
||||
// here is where we would implement server sharding
|
||||
return sessionUrl, nil
|
||||
},
|
||||
}
|
||||
|
||||
sessionCache = ttlcache.New(
|
||||
@@ -238,36 +260,20 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
sessionEventListener := sessionEventListener{
|
||||
sessionCache: sessionCache,
|
||||
logger: logger,
|
||||
counter: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: metrics.Namespace,
|
||||
Subsystem: metrics.Subsystem,
|
||||
Name: "outdated_sessions_count",
|
||||
Help: "Counts outdated session events",
|
||||
}),
|
||||
counter: m.OutdatedSessionsCounter,
|
||||
}
|
||||
jmapClient.AddSessionEventListener(&sessionEventListener)
|
||||
|
||||
// A channel to process SSE Events with a single worker.
|
||||
eventChannel := make(chan Event, eventChannelSize)
|
||||
{
|
||||
totalWorkerBufferMetric, err := prometheus.NewConstMetric(prometheus.NewDesc(
|
||||
prometheus.BuildFQName(metrics.Namespace, metrics.Subsystem, "event_buffer_size"),
|
||||
"Size of the buffer channel for server-sent events to process",
|
||||
nil,
|
||||
nil,
|
||||
), prometheus.GaugeValue, float64(eventChannelSize))
|
||||
eventBufferSizeMetric, err := prometheus.NewConstMetric(m.EventBufferSizeDesc, prometheus.GaugeValue, float64(eventChannelSize))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to create event_buffer_size metric")
|
||||
logger.Warn().Err(err).Msgf("failed to create metric %v", m.EventBufferSizeDesc.String())
|
||||
} else {
|
||||
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkerBufferMetric})
|
||||
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: eventBufferSizeMetric})
|
||||
}
|
||||
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: metrics.Namespace,
|
||||
Subsystem: metrics.Subsystem,
|
||||
Name: "event_buffer_queued",
|
||||
Help: "Number of queued server-sent events",
|
||||
}, func() float64 {
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.EventBufferQueuedOpts, func() float64 {
|
||||
return float64(len(eventChannel))
|
||||
}))
|
||||
}
|
||||
@@ -282,60 +288,35 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
sseServer.OnUnsubscribe = func(streamID string, sub *sse.Subscriber) {
|
||||
sseSubscribers.Add(-1)
|
||||
}
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: metrics.Namespace,
|
||||
Subsystem: metrics.Subsystem,
|
||||
Name: "sse_subscribers",
|
||||
Help: "Number of subscribers for server-sent event streams",
|
||||
}, func() float64 {
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.SSESubscribersOpts, func() float64 {
|
||||
return float64(sseSubscribers.Load())
|
||||
}))
|
||||
}
|
||||
|
||||
jobsChannel := make(chan Job, workerQueueSize)
|
||||
{
|
||||
totalWorkerBufferMetric, err := prometheus.NewConstMetric(prometheus.NewDesc(
|
||||
prometheus.BuildFQName(metrics.Namespace, metrics.Subsystem, "workers_buffer_size"),
|
||||
"Size of the buffer channel for background worker jobs",
|
||||
nil,
|
||||
nil,
|
||||
), prometheus.GaugeValue, float64(workerQueueSize))
|
||||
totalWorkerBufferMetric, err := prometheus.NewConstMetric(m.WorkersBufferSizeDesc, prometheus.GaugeValue, float64(workerQueueSize))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to create workers_buffer_size metric")
|
||||
logger.Warn().Err(err).Msgf("failed to create metric %v", m.WorkersBufferSizeDesc.String())
|
||||
} else {
|
||||
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkerBufferMetric})
|
||||
}
|
||||
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: metrics.Namespace,
|
||||
Subsystem: metrics.Subsystem,
|
||||
Name: "workers_buffer_queued",
|
||||
Help: "Number of queued background jobs",
|
||||
}, func() float64 {
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.WorkersBufferQueuedOpts, func() float64 {
|
||||
return float64(len(jobsChannel))
|
||||
}))
|
||||
}
|
||||
|
||||
var busyWorkers atomic.Int32
|
||||
{
|
||||
totalWorkersMetric, err := prometheus.NewConstMetric(prometheus.NewDesc(
|
||||
prometheus.BuildFQName(metrics.Namespace, metrics.Subsystem, "workers_total"),
|
||||
"Total amount of background job workers",
|
||||
nil,
|
||||
nil,
|
||||
), prometheus.GaugeValue, float64(workerPoolSize))
|
||||
totalWorkersMetric, err := prometheus.NewConstMetric(m.TotalWorkersDesc, prometheus.GaugeValue, float64(workerPoolSize))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to create workers_total metric")
|
||||
logger.Warn().Err(err).Msgf("failed to create metric %v", m.TotalWorkersDesc.String())
|
||||
} else {
|
||||
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkersMetric})
|
||||
}
|
||||
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: metrics.Namespace,
|
||||
Subsystem: metrics.Subsystem,
|
||||
Name: "workers_busy",
|
||||
Help: "Number of background job workers that are currently busy executing jobs",
|
||||
}, func() float64 {
|
||||
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.BusyWorkersOpts, func() float64 {
|
||||
return float64(busyWorkers.Load())
|
||||
}))
|
||||
}
|
||||
@@ -382,7 +363,7 @@ func (g *Groupware) worker(jobs <-chan Job, busy *atomic.Int32) {
|
||||
logger := log.From(job.logger.With().Str(logJobDescription, job.description).Uint64(logJobId, job.id))
|
||||
job.job(job.id, logger)
|
||||
if logger.Trace().Enabled() {
|
||||
logger.Trace().Msgf("finished job %d [%s] in %v", job.id, job.description, time.Since(before)) // TODO remove
|
||||
logger.Trace().Msgf("finished job %d [%s] in %v", job.id, job.description, time.Since(before))
|
||||
}
|
||||
busy.Add(-1)
|
||||
}
|
||||
@@ -469,274 +450,6 @@ func (g *Groupware) session(user User, _ *http.Request, _ context.Context, _ *lo
|
||||
return jmap.Session{}, false, nil
|
||||
}
|
||||
|
||||
// using a wrapper class for requests, to group multiple parameters, really to avoid crowding the
|
||||
// API of handlers but also to make it easier to expand it in the future without having to modify
|
||||
// the parameter list of every single handler function
|
||||
type Request struct {
|
||||
g *Groupware
|
||||
user User
|
||||
r *http.Request
|
||||
ctx context.Context
|
||||
logger *log.Logger
|
||||
session *jmap.Session
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
body any
|
||||
status int
|
||||
err *Error
|
||||
etag jmap.State
|
||||
sessionState jmap.SessionState
|
||||
}
|
||||
|
||||
func errorResponse(err *Error) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
err: err,
|
||||
etag: "",
|
||||
sessionState: "",
|
||||
}
|
||||
}
|
||||
|
||||
func response(body any, sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func etagResponse(body any, sessionState jmap.SessionState, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: etag,
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func etagOnlyResponse(body any, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: etag,
|
||||
sessionState: "",
|
||||
}
|
||||
}
|
||||
|
||||
func noContentResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNoContent,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func acceptedResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusAccepted,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func timeoutResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusRequestTimeout,
|
||||
err: nil,
|
||||
etag: "",
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func notFoundResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNotFound,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Request) push(typ string, event any) {
|
||||
r.g.push(r.user, typ, event)
|
||||
}
|
||||
|
||||
func (r Request) GetUser() User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
func (r Request) GetRequestId() string {
|
||||
return chimiddleware.GetReqID(r.ctx)
|
||||
}
|
||||
|
||||
func (r Request) GetTraceId() string {
|
||||
return groupwaremiddleware.GetTraceID(r.ctx)
|
||||
}
|
||||
|
||||
func (r Request) GetAccountId() string {
|
||||
accountId := chi.URLParam(r.r, UriParamAccount)
|
||||
return r.session.MailAccountId(accountId)
|
||||
}
|
||||
|
||||
func (r Request) GetAccount() (jmap.SessionAccount, *Error) {
|
||||
accountId := r.GetAccountId()
|
||||
|
||||
account, ok := r.session.Accounts[accountId]
|
||||
if !ok {
|
||||
r.logger.Debug().Msgf("failed to find account '%v'", accountId)
|
||||
// TODO metric for inexistent accounts
|
||||
return jmap.SessionAccount{}, apiError(r.errorId(), ErrorNonExistingAccount,
|
||||
withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))),
|
||||
withSource(&ErrorSource{Parameter: UriParamAccount}),
|
||||
)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r Request) parameterError(param string, detail string) *Error {
|
||||
return r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(detail),
|
||||
withSource(&ErrorSource{Parameter: param}))
|
||||
}
|
||||
|
||||
func (r Request) parameterErrorResponse(param string, detail string) Response {
|
||||
return errorResponse(r.parameterError(param, detail))
|
||||
}
|
||||
|
||||
func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(str, 10, 0)
|
||||
if err != nil {
|
||||
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
||||
// strconv.ParseInt: parsing \"a\": invalid syntax
|
||||
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return int(value), true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseUint(str, 10, 0)
|
||||
if err != nil {
|
||||
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
||||
// strconv.ParseInt: parsing \"a\": invalid syntax
|
||||
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return uint(value), true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(time.RFC3339, str)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Invalid RFC3339 value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return time.Time{}, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return t, true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Invalid boolean value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
func (r Request) body(target any) *Error {
|
||||
body := r.r.Body
|
||||
defer func(b io.ReadCloser) {
|
||||
err := b.Close()
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("failed to close request body")
|
||||
}
|
||||
}(body)
|
||||
|
||||
err := json.NewDecoder(body).Decode(target)
|
||||
if err != nil {
|
||||
return r.observedParameterError(ErrorInvalidRequestBody, withSource(&ErrorSource{Pointer: "/"})) // we don't get any details here
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Request) observe(obs prometheus.Observer, value float64) {
|
||||
metrics.WithExemplar(obs, value, r.GetRequestId(), r.GetTraceId())
|
||||
}
|
||||
|
||||
func (r Request) observeParameterError(err *Error) *Error {
|
||||
if err != nil {
|
||||
r.g.metrics.ParameterErrorCounter.WithLabelValues(err.Code).Inc()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r Request) observeJmapError(jerr jmap.Error) jmap.Error {
|
||||
if jerr != nil {
|
||||
r.g.metrics.JmapErrorCounter.WithLabelValues(r.session.JmapEndpoint, strconv.Itoa(jerr.Code())).Inc()
|
||||
}
|
||||
return jerr
|
||||
}
|
||||
|
||||
func (g *Groupware) log(error *Error) {
|
||||
var level *zerolog.Event
|
||||
if error.NumStatus < 300 {
|
||||
|
||||
259
services/groupware/pkg/groupware/groupware_request.go
Normal file
259
services/groupware/pkg/groupware/groupware_request.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics"
|
||||
groupwaremiddleware "github.com/opencloud-eu/opencloud/services/groupware/pkg/middleware"
|
||||
)
|
||||
|
||||
// using a wrapper class for requests, to group multiple parameters, really to avoid crowding the
|
||||
// API of handlers but also to make it easier to expand it in the future without having to modify
|
||||
// the parameter list of every single handler function
|
||||
type Request struct {
|
||||
g *Groupware
|
||||
user User
|
||||
r *http.Request
|
||||
ctx context.Context
|
||||
logger *log.Logger
|
||||
session *jmap.Session
|
||||
}
|
||||
|
||||
func (r Request) push(typ string, event any) {
|
||||
r.g.push(r.user, typ, event)
|
||||
}
|
||||
|
||||
func (r Request) GetUser() User {
|
||||
return r.user
|
||||
}
|
||||
|
||||
func (r Request) GetRequestId() string {
|
||||
return chimiddleware.GetReqID(r.ctx)
|
||||
}
|
||||
|
||||
func (r Request) GetTraceId() string {
|
||||
return groupwaremiddleware.GetTraceID(r.ctx)
|
||||
}
|
||||
|
||||
var (
|
||||
errNoPrimaryAccountFallback = errors.New("no primary account fallback")
|
||||
errNoPrimaryAccountForMail = errors.New("no primary account for mail")
|
||||
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")
|
||||
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
|
||||
// errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
|
||||
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
|
||||
)
|
||||
|
||||
func (r Request) GetAccountIdWithoutFallback() (string, *Error) {
|
||||
accountId := chi.URLParam(r.r, UriParamAccountId)
|
||||
if accountId == "" || accountId == defaultAccountId {
|
||||
r.logger.Error().Err(errNoPrimaryAccountFallback).Msg("failed to determine the accountId")
|
||||
return "", apiError(r.errorId(), ErrorNonExistingAccount,
|
||||
withDetail("Failed to determine the account to use"),
|
||||
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
||||
)
|
||||
}
|
||||
return accountId, nil
|
||||
}
|
||||
|
||||
func (r Request) getAccountId(fallback string, err error) (string, *Error) {
|
||||
accountId := chi.URLParam(r.r, UriParamAccountId)
|
||||
if accountId == "" || accountId == defaultAccountId {
|
||||
accountId = fallback
|
||||
}
|
||||
if accountId == "" {
|
||||
r.logger.Error().Err(err).Msg("failed to determine the accountId")
|
||||
return "", apiError(r.errorId(), ErrorNonExistingAccount,
|
||||
withDetail("Failed to determine the account to use"),
|
||||
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
||||
)
|
||||
}
|
||||
return accountId, nil
|
||||
}
|
||||
|
||||
func (r Request) GetAccountIdForMail() (string, *Error) {
|
||||
return r.getAccountId(r.session.PrimaryAccounts.Mail, errNoPrimaryAccountForMail)
|
||||
}
|
||||
|
||||
func (r Request) GetAccountIdForBlob() (string, *Error) {
|
||||
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForBlob)
|
||||
}
|
||||
|
||||
func (r Request) GetAccountIdForVacationResponse() (string, *Error) {
|
||||
return r.getAccountId(r.session.PrimaryAccounts.VacationResponse, errNoPrimaryAccountForVacationResponse)
|
||||
}
|
||||
|
||||
func (r Request) GetAccountIdForSubmission() (string, *Error) {
|
||||
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission)
|
||||
}
|
||||
|
||||
func (r Request) GetAccountForMail() (jmap.SessionAccount, *Error) {
|
||||
accountId, err := r.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return jmap.SessionAccount{}, err
|
||||
}
|
||||
|
||||
account, ok := r.session.Accounts[accountId]
|
||||
if !ok {
|
||||
r.logger.Debug().Msgf("failed to find account '%v'", accountId)
|
||||
// TODO metric for inexistent accounts
|
||||
return jmap.SessionAccount{}, apiError(r.errorId(), ErrorNonExistingAccount,
|
||||
withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))),
|
||||
withSource(&ErrorSource{Parameter: UriParamAccountId}),
|
||||
)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (r Request) parameterError(param string, detail string) *Error {
|
||||
return r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(detail),
|
||||
withSource(&ErrorSource{Parameter: param}))
|
||||
}
|
||||
|
||||
func (r Request) parameterErrorResponse(param string, detail string) Response {
|
||||
return errorResponse(r.parameterError(param, detail))
|
||||
}
|
||||
|
||||
func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseInt(str, 10, 0)
|
||||
if err != nil {
|
||||
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
||||
// strconv.ParseInt: parsing \"a\": invalid syntax
|
||||
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return int(value), true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
value, err := strconv.ParseUint(str, 10, 0)
|
||||
if err != nil {
|
||||
// don't include the original error, as it leaks too much about our implementation, e.g.:
|
||||
// strconv.ParseInt: parsing \"a\": invalid syntax
|
||||
msg := fmt.Sprintf("Invalid numeric value for query parameter '%v': '%s'", param, log.SafeString(str))
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return uint(value), true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(time.RFC3339, str)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Invalid RFC3339 value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return time.Time{}, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return t, true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
str := q.Get(param)
|
||||
if str == "" {
|
||||
return defaultValue, false, nil
|
||||
}
|
||||
|
||||
b, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Invalid boolean value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
return defaultValue, true, r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
)
|
||||
}
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
func (r Request) body(target any) *Error {
|
||||
body := r.r.Body
|
||||
defer func(b io.ReadCloser) {
|
||||
err := b.Close()
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("failed to close request body")
|
||||
}
|
||||
}(body)
|
||||
|
||||
err := json.NewDecoder(body).Decode(target)
|
||||
if err != nil {
|
||||
return r.observedParameterError(ErrorInvalidRequestBody, withSource(&ErrorSource{Pointer: "/"})) // we don't get any details here
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Request) observe(obs prometheus.Observer, value float64) {
|
||||
metrics.WithExemplar(obs, value, r.GetRequestId(), r.GetTraceId())
|
||||
}
|
||||
|
||||
func (r Request) observeParameterError(err *Error) *Error {
|
||||
if err != nil {
|
||||
r.g.metrics.ParameterErrorCounter.WithLabelValues(err.Code).Inc()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r Request) observeJmapError(jerr jmap.Error) jmap.Error {
|
||||
if jerr != nil {
|
||||
r.g.metrics.JmapErrorCounter.WithLabelValues(r.session.JmapEndpoint, strconv.Itoa(jerr.Code())).Inc()
|
||||
}
|
||||
return jerr
|
||||
}
|
||||
95
services/groupware/pkg/groupware/groupware_response.go
Normal file
95
services/groupware/pkg/groupware/groupware_response.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
body any
|
||||
status int
|
||||
err *Error
|
||||
etag jmap.State
|
||||
sessionState jmap.SessionState
|
||||
}
|
||||
|
||||
func errorResponse(err *Error) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
err: err,
|
||||
etag: "",
|
||||
sessionState: "",
|
||||
}
|
||||
}
|
||||
|
||||
func response(body any, sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func etagResponse(body any, sessionState jmap.SessionState, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: etag,
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func etagOnlyResponse(body any, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: etag,
|
||||
sessionState: "",
|
||||
}
|
||||
}
|
||||
|
||||
func noContentResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNoContent,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func acceptedResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusAccepted,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func timeoutResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusRequestTimeout,
|
||||
err: nil,
|
||||
etag: "",
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func notFoundResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNotFound,
|
||||
err: nil,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
UriParamAccount = "accountid"
|
||||
defaultAccountId = "*"
|
||||
|
||||
UriParamAccountId = "accountid"
|
||||
UriParamMailboxId = "mailbox"
|
||||
UriParamMessageId = "messageid"
|
||||
UriParamBlobId = "blobid"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
@@ -69,17 +70,23 @@ func (s failedSession) Since() time.Time {
|
||||
var _ cachedSession = failedSession{}
|
||||
|
||||
type sessionCacheLoader struct {
|
||||
logger *log.Logger
|
||||
jmapClient *jmap.Client
|
||||
errorTtl time.Duration
|
||||
logger *log.Logger
|
||||
sessionUrlProvider func(username string) (*url.URL, *GroupwareError)
|
||||
jmapClient *jmap.Client
|
||||
errorTtl time.Duration
|
||||
}
|
||||
|
||||
func (l *sessionCacheLoader) Load(c *ttlcache.Cache[sessionKey, cachedSession], key sessionKey) *ttlcache.Item[sessionKey, cachedSession] {
|
||||
username := usernameFromSessionKey(key)
|
||||
session, err := l.jmapClient.FetchSession(username, l.logger)
|
||||
if err != nil {
|
||||
l.logger.Warn().Str("username", username).Err(err).Msgf("failed to create session for '%v'", key)
|
||||
return c.Set(key, failedSession{since: time.Now(), err: groupwareErrorFromJmap(err)}, l.errorTtl)
|
||||
sessionUrl, gwerr := l.sessionUrlProvider(username)
|
||||
if gwerr != nil {
|
||||
l.logger.Warn().Str("username", username).Str("code", gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
|
||||
return c.Set(key, failedSession{since: time.Now(), err: gwerr}, l.errorTtl)
|
||||
}
|
||||
session, jerr := l.jmapClient.FetchSession(sessionUrl, username, l.logger)
|
||||
if jerr != nil {
|
||||
l.logger.Warn().Str("username", username).Err(jerr).Msgf("failed to create session for '%v'", key)
|
||||
return c.Set(key, failedSession{since: time.Now(), err: groupwareErrorFromJmap(jerr)}, l.errorTtl)
|
||||
} else {
|
||||
l.logger.Debug().Str("username", username).Msgf("successfully created session for '%v'", key)
|
||||
return c.Set(key, succeededSession{since: time.Now(), session: session}, ttlcache.DefaultTTL) // use the TTL configured on the Cache
|
||||
|
||||
@@ -2,6 +2,7 @@ package metrics
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -17,16 +18,27 @@ const (
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
SessionCacheDesc *prometheus.Desc
|
||||
SessionCacheDesc *prometheus.Desc
|
||||
EventBufferSizeDesc *prometheus.Desc
|
||||
EventBufferQueuedOpts prometheus.GaugeOpts
|
||||
SSESubscribersOpts prometheus.GaugeOpts
|
||||
WorkersBufferSizeDesc *prometheus.Desc
|
||||
WorkersBufferQueuedOpts prometheus.GaugeOpts
|
||||
TotalWorkersDesc *prometheus.Desc
|
||||
BusyWorkersOpts prometheus.GaugeOpts
|
||||
|
||||
JmapErrorCounter *prometheus.CounterVec
|
||||
ParameterErrorCounter *prometheus.CounterVec
|
||||
AuthenticationFailureCounter prometheus.Counter
|
||||
SessionFailureCounter prometheus.Counter
|
||||
SSEEventsCounter *prometheus.CounterVec
|
||||
OutdatedSessionsCounter prometheus.Counter
|
||||
|
||||
SuccessfulRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestStatusPerEndpointCounter *prometheus.CounterVec
|
||||
SuccessfulRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestPerEndpointCounter *prometheus.CounterVec
|
||||
FailedRequestStatusPerEndpointCounter *prometheus.CounterVec
|
||||
ResponseBodyReadingErrorPerEndpointCounter *prometheus.CounterVec
|
||||
ResponseBodyUnmarshallingErrorPerEndpointCounter *prometheus.CounterVec
|
||||
|
||||
EmailByIdDuration *prometheus.HistogramVec
|
||||
EmailSameSenderDuration *prometheus.HistogramVec
|
||||
@@ -92,7 +104,7 @@ var Values = struct {
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New(logger *log.Logger) *Metrics {
|
||||
func New(registerer prometheus.Registerer, logger *log.Logger) *Metrics {
|
||||
m := &Metrics{
|
||||
SessionCacheDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, Subsystem, "session_cache"),
|
||||
@@ -100,6 +112,48 @@ func New(logger *log.Logger) *Metrics {
|
||||
[]string{Labels.SessionCacheType},
|
||||
nil,
|
||||
),
|
||||
EventBufferSizeDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, Subsystem, "event_buffer_size"),
|
||||
"Size of the buffer channel for server-sent events to process",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
EventBufferQueuedOpts: prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "event_buffer_queued",
|
||||
Help: "Number of queued server-sent events",
|
||||
},
|
||||
SSESubscribersOpts: prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "sse_subscribers",
|
||||
Help: "Number of subscribers for server-sent event streams",
|
||||
},
|
||||
WorkersBufferSizeDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, Subsystem, "workers_buffer_size"),
|
||||
"Size of the buffer channel for background worker jobs",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
WorkersBufferQueuedOpts: prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "workers_buffer_queued",
|
||||
Help: "Number of queued background jobs",
|
||||
},
|
||||
TotalWorkersDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(Namespace, Subsystem, "workers_total"),
|
||||
"Total amount of background job workers",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
BusyWorkersOpts: prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "workers_busy",
|
||||
Help: "Number of background job workers that are currently busy executing jobs",
|
||||
},
|
||||
AuthenticationFailureCounter: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
@@ -148,12 +202,30 @@ func New(logger *log.Logger) *Metrics {
|
||||
Name: "jmap_requests_failures_status_count",
|
||||
Help: "Number of JMAP requests",
|
||||
}, []string{Labels.Endpoint, Labels.HttpStatusCode}),
|
||||
ResponseBodyReadingErrorPerEndpointCounter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "jmap_requests_body_reading_errors_count",
|
||||
Help: "Number of JMAP body reading errors",
|
||||
}, []string{Labels.Endpoint}),
|
||||
ResponseBodyUnmarshallingErrorPerEndpointCounter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "jmap_requests_body_unmarshalling_errors_count",
|
||||
Help: "Number of JMAP body unmarshalling errors",
|
||||
}, []string{Labels.Endpoint}),
|
||||
SSEEventsCounter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "sse_events_count",
|
||||
Help: "Number of Server-Side Events that have been sent",
|
||||
}, []string{Labels.SSEType}),
|
||||
OutdatedSessionsCounter: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "outdated_sessions_count",
|
||||
Help: "Counts outdated session events",
|
||||
}),
|
||||
EmailByIdDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
@@ -177,7 +249,7 @@ func New(logger *log.Logger) *Metrics {
|
||||
}, []string{Labels.Endpoint}),
|
||||
}
|
||||
|
||||
registerAll(m, logger)
|
||||
registerAll(registerer, m, logger)
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -186,23 +258,31 @@ func WithExemplar(obs prometheus.Observer, value float64, requestId string, trac
|
||||
obs.(prometheus.ExemplarObserver).ObserveWithExemplar(value, prometheus.Labels{Labels.RequestId: requestId, Labels.TraceId: traceId})
|
||||
}
|
||||
|
||||
func registerAll(m any, logger *log.Logger) {
|
||||
func registerAll(registerer prometheus.Registerer, m any, logger *log.Logger) {
|
||||
r := reflect.ValueOf(m)
|
||||
if r.Kind() == reflect.Pointer {
|
||||
r = r.Elem()
|
||||
}
|
||||
total := 0
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
for i := 0; i < r.NumField(); i++ {
|
||||
n := r.Type().Field(i).Name
|
||||
f := r.Field(i)
|
||||
v := f.Interface()
|
||||
c, ok := v.(prometheus.Collector)
|
||||
if ok {
|
||||
err := prometheus.Register(c)
|
||||
total++
|
||||
err := registerer.Register(c)
|
||||
if err != nil {
|
||||
failed++
|
||||
logger.Warn().Err(err).Msgf("failed to register metric '%s' (%T)", n, c)
|
||||
} else {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Debug().Msgf("registered %d/%d metrics successfully (%d failed)", succeeded, total, failed)
|
||||
}
|
||||
|
||||
type ConstMetricCollector struct {
|
||||
@@ -221,26 +301,43 @@ type LoggingPrometheusRegisterer struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewLoggingPrometheusRegisterer(delegate prometheus.Registerer, logger *log.Logger) LoggingPrometheusRegisterer {
|
||||
return LoggingPrometheusRegisterer{
|
||||
func NewLoggingPrometheusRegisterer(delegate prometheus.Registerer, logger *log.Logger) *LoggingPrometheusRegisterer {
|
||||
return &LoggingPrometheusRegisterer{
|
||||
delegate: delegate,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r LoggingPrometheusRegisterer) Register(c prometheus.Collector) error {
|
||||
func (r *LoggingPrometheusRegisterer) Register(c prometheus.Collector) error {
|
||||
err := r.delegate.Register(c)
|
||||
if err != nil {
|
||||
r.logger.Warn().Err(err).Msgf("failed to register metric")
|
||||
switch err.(type) {
|
||||
case prometheus.AlreadyRegisteredError:
|
||||
// silently ignore this error, as this case can happen when the suture service decides to restart
|
||||
err = nil
|
||||
default:
|
||||
r.logger.Warn().Err(err).Msgf("failed to register metric")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (r LoggingPrometheusRegisterer) MustRegister(...prometheus.Collector) {
|
||||
panic("don't use MustRegister")
|
||||
|
||||
func (r *LoggingPrometheusRegisterer) MustRegister(collectors ...prometheus.Collector) {
|
||||
for _, c := range collectors {
|
||||
r.Register(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (r LoggingPrometheusRegisterer) Unregister(c prometheus.Collector) bool {
|
||||
func (r *LoggingPrometheusRegisterer) Unregister(c prometheus.Collector) bool {
|
||||
return r.delegate.Unregister(c)
|
||||
}
|
||||
|
||||
var _ prometheus.Registerer = LoggingPrometheusRegisterer{}
|
||||
var _ prometheus.Registerer = &LoggingPrometheusRegisterer{}
|
||||
|
||||
func Endpoint(endpoint string) prometheus.Labels {
|
||||
return prometheus.Labels{Labels.Endpoint: endpoint}
|
||||
}
|
||||
|
||||
func EndpointAndStatus(endpoint string, status int) prometheus.Labels {
|
||||
return prometheus.Labels{Labels.Endpoint: endpoint, Labels.HttpStatusCode: strconv.Itoa(status)}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,29 @@ import (
|
||||
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) {
|
||||
level := logger.Debug()
|
||||
if !level.Enabled() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
wrap := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
next.ServeHTTP(wrap, r)
|
||||
|
||||
level := logger.Debug()
|
||||
err := recover()
|
||||
if err != nil {
|
||||
level = logger.Error()
|
||||
}
|
||||
|
||||
if !level.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
switch e := err.(type) {
|
||||
case error:
|
||||
level = level.Err(e)
|
||||
default:
|
||||
level = level.Any("panic", e)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
requestID := middleware.GetReqID(ctx)
|
||||
|
||||
Reference in New Issue
Block a user