mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-10 22:19:17 -06:00
groupware: Etag handling
* implement correct Etag and If-None-Match handling, responding with 304 Not Modified if they match * introduce SessionState and State string type aliases to ensure we are using the correct fields for those, respectively * extract the SessionState from the JMAP response bodies in the groupware framework instead of having to do that in every single groupware API * use uint instead of int in some places to clarify that the values are >= 0 * trace-log how long a Session was held in cache before being evicted * add Trace-Id header handling: add to response when specified in request, and implement a custom request logger to include it as a field * implement a more compact trace-logging of all the methods and URIs that are served, to put them into a single log entry instead of creating one log entry for every URI
This commit is contained in:
@@ -10,12 +10,11 @@ import (
|
||||
)
|
||||
|
||||
type BlobResponse struct {
|
||||
Blob *Blob `json:"blob,omitempty"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Blob *Blob `json:"blob,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, id string) (BlobResponse, Error) {
|
||||
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(
|
||||
@@ -27,7 +26,7 @@ func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return BlobResponse{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return BlobResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (BlobResponse, Error) {
|
||||
@@ -43,17 +42,16 @@ func (j *Client) GetBlob(accountId string, session *Session, ctx context.Context
|
||||
return BlobResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
get := response.List[0]
|
||||
return BlobResponse{Blob: &get, State: response.State, SessionState: body.SessionState}, nil
|
||||
return BlobResponse{Blob: &get, State: response.State}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type UploadedBlob struct {
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) UploadBlobStream(accountId string, session *Session, ctx context.Context, logger *log.Logger, contentType string, body io.Reader) (UploadedBlob, Error) {
|
||||
@@ -75,7 +73,7 @@ func (j *Client) DownloadBlobStream(accountId string, blobId string, name string
|
||||
return j.blob.DownloadBinary(ctx, logger, session, downloadUrl)
|
||||
}
|
||||
|
||||
func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte, contentType string) (UploadedBlob, Error) {
|
||||
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)
|
||||
@@ -108,7 +106,7 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return UploadedBlob{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return UploadedBlob{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (UploadedBlob, Error) {
|
||||
@@ -143,12 +141,11 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
|
||||
get := getResponse.List[0]
|
||||
|
||||
return UploadedBlob{
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
State: getResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
State: getResponse.State,
|
||||
}, nil
|
||||
})
|
||||
|
||||
|
||||
@@ -23,27 +23,26 @@ const (
|
||||
)
|
||||
|
||||
type Emails struct {
|
||||
Emails []Email `json:"emails,omitempty"`
|
||||
Total int `json:"total,omitzero"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Offset int `json:"offset,omitzero"`
|
||||
State string `json:"state,omitempty"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Emails []Email `json:"emails,omitempty"`
|
||||
Total uint `json:"total,omitzero"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
Offset uint `json:"offset,omitzero"`
|
||||
State State `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes int) (Emails, Error) {
|
||||
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)
|
||||
|
||||
get := EmailGetCommand{AccountId: aid, Ids: ids, FetchAllBodyValues: fetchBodies}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
get.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
|
||||
cmd, err := request(invocation(CommandEmailGet, get, "0"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return Emails{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return Emails{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Emails, Error) {
|
||||
var response EmailGetResponse
|
||||
@@ -52,14 +51,14 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
|
||||
logger.Error().Err(err)
|
||||
return Emails{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
return Emails{Emails: response.List, State: response.State, SessionState: body.SessionState}, nil
|
||||
return Emails{Emails: response.List, State: response.State}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (Emails, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Int(logLimit, limit)
|
||||
return z.Bool(logFetchBodies, fetchBodies).Uint(logOffset, offset).Uint(logLimit, limit)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
@@ -69,10 +68,10 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
if offset >= 0 {
|
||||
if offset > 0 {
|
||||
query.Position = offset
|
||||
}
|
||||
if limit >= 0 {
|
||||
if limit > 0 {
|
||||
query.Limit = limit
|
||||
}
|
||||
|
||||
@@ -81,7 +80,7 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"},
|
||||
}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
get.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return Emails{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return Emails{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Emails, Error) {
|
||||
@@ -109,12 +108,11 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
}
|
||||
|
||||
return Emails{
|
||||
Emails: getResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Offset: queryResponse.Position,
|
||||
SessionState: body.SessionState,
|
||||
State: getResponse.State,
|
||||
Emails: getResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Offset: queryResponse.Position,
|
||||
State: getResponse.State,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -122,14 +120,13 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
|
||||
type EmailsSince struct {
|
||||
Destroyed []string `json:"destroyed,omitzero"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges,omitzero"`
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
Created []Email `json:"created,omitempty"`
|
||||
Updated []Email `json:"updated,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
SessionState string `json:"sessionState"`
|
||||
State State `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, since string, fetchBodies bool, maxBodyValueBytes int, maxChanges int) (EmailsSince, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
|
||||
@@ -139,7 +136,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
AccountId: aid,
|
||||
SinceState: since,
|
||||
}
|
||||
if maxChanges >= 0 {
|
||||
if maxChanges > 0 {
|
||||
changes.MaxChanges = maxChanges
|
||||
}
|
||||
|
||||
@@ -148,7 +145,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
|
||||
}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
getCreated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
getUpdated := EmailGetRefCommand{
|
||||
@@ -156,7 +153,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
|
||||
}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
|
||||
@@ -167,7 +164,7 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return EmailsSince{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailsSince, Error) {
|
||||
@@ -199,12 +196,11 @@ func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx
|
||||
Created: createdResponse.List,
|
||||
Updated: createdResponse.List,
|
||||
State: createdResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, since string, fetchBodies bool, maxBodyValueBytes int, maxChanges int) (EmailsSince, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
|
||||
@@ -214,7 +210,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
AccountId: aid,
|
||||
SinceState: since,
|
||||
}
|
||||
if maxChanges >= 0 {
|
||||
if maxChanges > 0 {
|
||||
changes.MaxChanges = maxChanges
|
||||
}
|
||||
|
||||
@@ -223,7 +219,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/created", ResultOf: "0"},
|
||||
}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
getCreated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
getUpdated := EmailGetRefCommand{
|
||||
@@ -231,7 +227,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
FetchAllBodyValues: fetchBodies,
|
||||
IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/updated", ResultOf: "0"},
|
||||
}
|
||||
if maxBodyValueBytes >= 0 {
|
||||
if maxBodyValueBytes > 0 {
|
||||
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
|
||||
}
|
||||
|
||||
@@ -241,7 +237,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
invocation(CommandEmailGet, getUpdated, "2"),
|
||||
)
|
||||
if err != nil {
|
||||
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return EmailsSince{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailsSince, Error) {
|
||||
@@ -273,24 +269,22 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
|
||||
Created: createdResponse.List,
|
||||
Updated: createdResponse.List,
|
||||
State: updatedResponse.State,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type EmailSnippetQueryResult struct {
|
||||
Snippets []SearchSnippet `json:"snippets,omitempty"`
|
||||
QueryState string `json:"queryState"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Position int `json:"position,omitzero"`
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
Snippets []SearchSnippet `json:"snippets,omitempty"`
|
||||
Total uint `json:"total"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
Position uint `json:"position,omitzero"`
|
||||
QueryState State `json:"queryState"`
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int) (EmailSnippetQueryResult, Error) {
|
||||
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 {
|
||||
return z.Int(logLimit, limit).Int(logOffset, offset)
|
||||
return z.Uint(logLimit, limit).Uint(logOffset, offset)
|
||||
})
|
||||
|
||||
query := EmailQueryCommand{
|
||||
@@ -300,10 +294,10 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement,
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
if offset >= 0 {
|
||||
if offset > 0 {
|
||||
query.Position = offset
|
||||
}
|
||||
if limit >= 0 {
|
||||
if limit > 0 {
|
||||
query.Limit = limit
|
||||
}
|
||||
|
||||
@@ -324,7 +318,7 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement,
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return EmailSnippetQueryResult{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return EmailSnippetQueryResult{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailSnippetQueryResult, Error) {
|
||||
@@ -343,27 +337,25 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement,
|
||||
}
|
||||
|
||||
return EmailSnippetQueryResult{
|
||||
Snippets: snippetResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
SessionState: body.SessionState,
|
||||
Snippets: snippetResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type EmailQueryResult struct {
|
||||
Results []Email `json:"results"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Position int `json:"position,omitzero"`
|
||||
QueryState string `json:"queryState"`
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
Emails []Email `json:"emails"`
|
||||
Total uint `json:"total"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
Position uint `json:"position,omitzero"`
|
||||
QueryState State `json:"queryState"`
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (EmailQueryResult, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies)
|
||||
@@ -376,10 +368,10 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
if offset >= 0 {
|
||||
if offset > 0 {
|
||||
query.Position = offset
|
||||
}
|
||||
if limit >= 0 {
|
||||
if limit > 0 {
|
||||
query.Limit = limit
|
||||
}
|
||||
|
||||
@@ -401,7 +393,7 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return EmailQueryResult{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return EmailQueryResult{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailQueryResult, Error) {
|
||||
@@ -418,12 +410,11 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
|
||||
}
|
||||
|
||||
return EmailQueryResult{
|
||||
Results: emailsResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
SessionState: body.SessionState,
|
||||
Emails: emailsResponse.List,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
@@ -435,15 +426,14 @@ type EmailWithSnippets struct {
|
||||
}
|
||||
|
||||
type EmailQueryWithSnippetsResult struct {
|
||||
Results []EmailWithSnippets `json:"results"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
Position int `json:"position,omitzero"`
|
||||
QueryState string `json:"queryState"`
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
Results []EmailWithSnippets `json:"results"`
|
||||
Total uint `json:"total"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
Position uint `json:"position,omitzero"`
|
||||
QueryState State `json:"queryState"`
|
||||
}
|
||||
|
||||
func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (EmailQueryWithSnippetsResult, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies)
|
||||
@@ -456,10 +446,10 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
if offset >= 0 {
|
||||
if offset > 0 {
|
||||
query.Position = offset
|
||||
}
|
||||
if limit >= 0 {
|
||||
if limit > 0 {
|
||||
query.Limit = limit
|
||||
}
|
||||
|
||||
@@ -492,7 +482,7 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return EmailQueryWithSnippetsResult{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return EmailQueryWithSnippetsResult{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailQueryWithSnippetsResult, Error) {
|
||||
@@ -536,26 +526,24 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
|
||||
}
|
||||
|
||||
return EmailQueryWithSnippetsResult{
|
||||
Results: results,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
SessionState: body.SessionState,
|
||||
Results: results,
|
||||
Total: queryResponse.Total,
|
||||
Limit: queryResponse.Limit,
|
||||
Position: queryResponse.Position,
|
||||
QueryState: queryResponse.QueryState,
|
||||
}, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type UploadedEmail struct {
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Id string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Sha512 string `json:"sha:512"`
|
||||
}
|
||||
|
||||
func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (UploadedEmail, Error) {
|
||||
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)
|
||||
@@ -587,7 +575,7 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
|
||||
invocation(CommandBlobGet, getHash, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
return UploadedEmail{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (UploadedEmail, Error) {
|
||||
@@ -622,23 +610,21 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
|
||||
get := getResponse.List[0]
|
||||
|
||||
return UploadedEmail{
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
SessionState: body.SessionState,
|
||||
Id: upload.Id,
|
||||
Size: upload.Size,
|
||||
Type: upload.Type,
|
||||
Sha512: get.DigestSha512,
|
||||
}, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type CreatedEmail struct {
|
||||
Email Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Email Email `json:"email"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger) (CreatedEmail, Error) {
|
||||
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(
|
||||
@@ -651,7 +637,7 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Sessi
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return CreatedEmail{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return CreatedEmail{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (CreatedEmail, Error) {
|
||||
@@ -681,17 +667,15 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Sessi
|
||||
}
|
||||
|
||||
return CreatedEmail{
|
||||
Email: created,
|
||||
State: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
Email: created,
|
||||
State: setResponse.NewState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type UpdatedEmails struct {
|
||||
Updated map[string]Email `json:"email"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Updated map[string]Email `json:"email"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// The Email/set method encompasses:
|
||||
@@ -702,7 +686,7 @@ type UpdatedEmails struct {
|
||||
// To create drafts, use the CreateEmail function instead.
|
||||
//
|
||||
// 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, Error) {
|
||||
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(
|
||||
@@ -713,7 +697,7 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return UpdatedEmails{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return UpdatedEmails{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (UpdatedEmails, Error) {
|
||||
@@ -728,19 +712,17 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
|
||||
// TODO(pbleser-oc) handle submission errors
|
||||
}
|
||||
return UpdatedEmails{
|
||||
Updated: setResponse.Updated,
|
||||
State: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
Updated: setResponse.Updated,
|
||||
State: setResponse.NewState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type DeletedEmails struct {
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger) (DeletedEmails, Error) {
|
||||
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(
|
||||
@@ -751,7 +733,7 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return DeletedEmails{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return DeletedEmails{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (DeletedEmails, Error) {
|
||||
@@ -765,13 +747,13 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi
|
||||
// error occured
|
||||
// TODO(pbleser-oc) handle submission errors
|
||||
}
|
||||
return DeletedEmails{State: setResponse.NewState, SessionState: body.SessionState}, nil
|
||||
return DeletedEmails{State: setResponse.NewState}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type SubmittedEmail struct {
|
||||
Id string `json:"id"`
|
||||
State string `json:"state"`
|
||||
State State `json:"state"`
|
||||
SendAt time.Time `json:"sendAt,omitzero"`
|
||||
ThreadId string `json:"threadId,omitempty"`
|
||||
UndoStatus EmailSubmissionUndoStatus `json:"undoStatus,omitempty"`
|
||||
@@ -792,11 +774,9 @@ type SubmittedEmail struct {
|
||||
//
|
||||
// [RFC8098]: https://datatracker.ietf.org/doc/html/rfc8098
|
||||
MdnBlobIds []string `json:"mdnBlobIds,omitempty"`
|
||||
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (SubmittedEmail, Error) {
|
||||
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{
|
||||
@@ -829,7 +809,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return SubmittedEmail{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return SubmittedEmail{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (SubmittedEmail, Error) {
|
||||
@@ -872,25 +852,19 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
submission := getResponse.List[0]
|
||||
|
||||
return SubmittedEmail{
|
||||
Id: submission.Id,
|
||||
State: setResponse.NewState,
|
||||
SendAt: submission.SendAt,
|
||||
ThreadId: submission.ThreadId,
|
||||
UndoStatus: submission.UndoStatus,
|
||||
Envelope: *submission.Envelope,
|
||||
DsnBlobIds: submission.DsnBlobIds,
|
||||
MdnBlobIds: submission.MdnBlobIds,
|
||||
SessionState: body.SessionState,
|
||||
Id: submission.Id,
|
||||
State: setResponse.NewState,
|
||||
SendAt: submission.SendAt,
|
||||
ThreadId: submission.ThreadId,
|
||||
UndoStatus: submission.UndoStatus,
|
||||
Envelope: *submission.Envelope,
|
||||
DsnBlobIds: submission.DsnBlobIds,
|
||||
MdnBlobIds: submission.MdnBlobIds,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type EmailsInThreadResult struct {
|
||||
Emails []Email `json:"emails"`
|
||||
SessionState string `json:"sessionState"`
|
||||
}
|
||||
|
||||
func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, fetchBodies bool, maxBodyValueBytes int) (EmailsInThreadResult, Error) {
|
||||
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 {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
|
||||
@@ -915,19 +889,16 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return EmailsInThreadResult{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return []Email{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailsInThreadResult, Error) {
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) ([]Email, Error) {
|
||||
var emailsResponse EmailGetResponse
|
||||
err = retrieveResponseMatchParameters(body, CommandEmailGet, "1", &emailsResponse)
|
||||
if err != nil {
|
||||
return EmailsInThreadResult{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
return []Email{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
return EmailsInThreadResult{
|
||||
Emails: emailsResponse.List,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
return emailsResponse.List, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -10,19 +10,18 @@ import (
|
||||
)
|
||||
|
||||
type Identities struct {
|
||||
Identities []Identity `json:"identities"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Identities []Identity `json:"identities"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#identityget
|
||||
func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (Identities, Error) {
|
||||
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"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return Identities{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return Identities{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Identities, Error) {
|
||||
var response IdentityGetResponse
|
||||
@@ -32,21 +31,19 @@ func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Con
|
||||
return Identities{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
return Identities{
|
||||
Identities: response.List,
|
||||
State: response.State,
|
||||
SessionState: body.SessionState,
|
||||
Identities: response.List,
|
||||
State: response.State,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type IdentitiesGetResponse struct {
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, Error) {
|
||||
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 {
|
||||
@@ -61,11 +58,11 @@ func (j *Client) GetIdentities(accountIds []string, session *Session, ctx contex
|
||||
cmd, err := request(calls...)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return IdentitiesGetResponse{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return IdentitiesGetResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentitiesGetResponse, Error) {
|
||||
identities := make(map[string][]Identity, len(uniqueAccountIds))
|
||||
lastState := ""
|
||||
var lastState State
|
||||
notFound := []string{}
|
||||
for i, accountId := range uniqueAccountIds {
|
||||
var response IdentityGetResponse
|
||||
@@ -81,23 +78,21 @@ func (j *Client) GetIdentities(accountIds []string, session *Session, ctx contex
|
||||
}
|
||||
|
||||
return IdentitiesGetResponse{
|
||||
Identities: identities,
|
||||
NotFound: structs.Uniq(notFound),
|
||||
State: lastState,
|
||||
SessionState: body.SessionState,
|
||||
Identities: identities,
|
||||
NotFound: structs.Uniq(notFound),
|
||||
State: lastState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type IdentitiesAndMailboxesGetResponse struct {
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
Identities map[string][]Identity `json:"identities,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State State `json:"state"`
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
}
|
||||
|
||||
func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesAndMailboxesGetResponse, Error) {
|
||||
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 {
|
||||
@@ -113,11 +108,11 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [
|
||||
cmd, err := request(calls...)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return IdentitiesAndMailboxesGetResponse{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return IdentitiesAndMailboxesGetResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, Error) {
|
||||
identities := make(map[string][]Identity, len(uniqueAccountIds))
|
||||
lastState := ""
|
||||
var lastState State
|
||||
notFound := []string{}
|
||||
for i, accountId := range uniqueAccountIds {
|
||||
var response IdentityGetResponse
|
||||
@@ -140,11 +135,10 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [
|
||||
}
|
||||
|
||||
return IdentitiesAndMailboxesGetResponse{
|
||||
Identities: identities,
|
||||
NotFound: structs.Uniq(notFound),
|
||||
State: lastState,
|
||||
SessionState: body.SessionState,
|
||||
Mailboxes: mailboxResponse.List,
|
||||
Identities: identities,
|
||||
NotFound: structs.Uniq(notFound),
|
||||
State: lastState,
|
||||
Mailboxes: mailboxResponse.List,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,20 +7,19 @@ import (
|
||||
)
|
||||
|
||||
type MailboxesResponse struct {
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
NotFound []any `json:"notFound"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
NotFound []any `json:"notFound"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// https://jmap.io/spec-mail.html#mailboxget
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, Error) {
|
||||
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"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return MailboxesResponse{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return MailboxesResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxesResponse, Error) {
|
||||
@@ -32,42 +31,37 @@ func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Cont
|
||||
}
|
||||
|
||||
return MailboxesResponse{
|
||||
Mailboxes: response.List,
|
||||
NotFound: response.NotFound,
|
||||
State: response.State,
|
||||
SessionState: body.SessionState,
|
||||
Mailboxes: response.List,
|
||||
NotFound: response.NotFound,
|
||||
State: response.State,
|
||||
}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
})
|
||||
}
|
||||
|
||||
type AllMailboxesResponse struct {
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
State string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) GetAllMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger) (AllMailboxesResponse, Error) {
|
||||
resp, err := j.GetMailbox(accountId, session, ctx, logger, nil)
|
||||
func (j *Client) GetAllMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger) (AllMailboxesResponse, SessionState, Error) {
|
||||
resp, sessionState, err := j.GetMailbox(accountId, session, ctx, logger, nil)
|
||||
if err != nil {
|
||||
return AllMailboxesResponse{}, err
|
||||
return AllMailboxesResponse{}, sessionState, err
|
||||
}
|
||||
return AllMailboxesResponse{
|
||||
Mailboxes: resp.Mailboxes,
|
||||
State: resp.State,
|
||||
SessionState: resp.SessionState,
|
||||
}, nil
|
||||
Mailboxes: resp.Mailboxes,
|
||||
State: resp.State,
|
||||
}, sessionState, nil
|
||||
}
|
||||
|
||||
type Mailboxes struct {
|
||||
// The list of mailboxes that were found using the specified search criteria.
|
||||
Mailboxes []Mailbox `json:"mailboxes,omitempty"`
|
||||
// The state of the search.
|
||||
State string `json:"state,omitempty"`
|
||||
// The state of the Session.
|
||||
SessionState string `json:"sessionState,omitempty"`
|
||||
State State `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (Mailboxes, Error) {
|
||||
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)
|
||||
|
||||
@@ -80,7 +74,7 @@ func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return Mailboxes{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return Mailboxes{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Mailboxes, Error) {
|
||||
@@ -90,6 +84,6 @@ func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context
|
||||
logger.Error().Err(err)
|
||||
return Mailboxes{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
return Mailboxes{Mailboxes: response.List, State: response.State, SessionState: body.SessionState}, nil
|
||||
return Mailboxes{Mailboxes: response.List, State: response.State}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ const (
|
||||
)
|
||||
|
||||
// https://jmap.io/spec-mail.html#vacationresponseget
|
||||
func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, Error) {
|
||||
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"))
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return VacationResponseGetResponse{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return VacationResponseGetResponse{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseGetResponse, Error) {
|
||||
var response VacationResponseGetResponse
|
||||
@@ -58,11 +58,10 @@ type VacationResponsePayload struct {
|
||||
|
||||
type VacationResponseChange struct {
|
||||
VacationResponse VacationResponse `json:"vacationResponse"`
|
||||
ResponseState string `json:"state"`
|
||||
SessionState string `json:"sessionState"`
|
||||
ResponseState State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, Error) {
|
||||
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)
|
||||
|
||||
@@ -86,7 +85,7 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Err(err)
|
||||
return VacationResponseChange{}, simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
return VacationResponseChange{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
|
||||
}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseChange, Error) {
|
||||
var setResponse VacationResponseSetResponse
|
||||
@@ -119,7 +118,6 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse
|
||||
return VacationResponseChange{
|
||||
VacationResponse: getResponse.List[0],
|
||||
ResponseState: setResponse.NewState,
|
||||
SessionState: body.SessionState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (j *Client) AddSessionEventListener(listener SessionEventListener) {
|
||||
j.sessionEventListeners.add(listener)
|
||||
}
|
||||
|
||||
func (j *Client) onSessionOutdated(session *Session, newSessionState string) {
|
||||
func (j *Client) onSessionOutdated(session *Session, newSessionState SessionState) {
|
||||
j.sessionEventListeners.signal(func(listener SessionEventListener) {
|
||||
listener.OnSessionOutdated(session, newSessionState)
|
||||
})
|
||||
|
||||
@@ -310,6 +310,10 @@ type SessionPrimaryAccounts struct {
|
||||
Websocket string `json:"urn:ietf:params:jmap:websocket"`
|
||||
}
|
||||
|
||||
type SessionState string
|
||||
|
||||
type State string
|
||||
|
||||
type SessionResponse struct {
|
||||
Capabilities SessionCapabilities `json:"capabilities"`
|
||||
|
||||
@@ -345,7 +349,7 @@ type SessionResponse struct {
|
||||
// The current value is also returned on the API Response object (see Section 3.4), allowing clients to quickly
|
||||
// determine if the session information has changed (e.g., an account has been added or removed),
|
||||
// so they need to refetch the object.
|
||||
State string `json:"state,omitempty"`
|
||||
State SessionState `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// SetError type values.
|
||||
@@ -667,7 +671,7 @@ type MailboxChangesCommand struct {
|
||||
// If supplied by the client, the value MUST be a positive integer greater than 0.
|
||||
//
|
||||
// If a value outside of this range is given, the server MUST reject the call with an invalidArguments error.
|
||||
MaxChanges int `json:"maxChanges,omitzero"`
|
||||
MaxChanges uint `json:"maxChanges,omitzero"`
|
||||
}
|
||||
|
||||
type MailboxFilterElement interface {
|
||||
@@ -962,7 +966,7 @@ type EmailQueryCommand struct {
|
||||
//
|
||||
// If the index is greater than or equal to the total number of objects in the results
|
||||
// list, then the ids array in the response will be empty, but this is not an error.
|
||||
Position int `json:"position,omitempty"`
|
||||
Position uint `json:"position,omitempty"`
|
||||
|
||||
// An Email id.
|
||||
//
|
||||
@@ -990,7 +994,7 @@ type EmailQueryCommand struct {
|
||||
// to the maximum; the new limit is returned with the response so the client is aware.
|
||||
//
|
||||
// If a negative value is given, the call MUST be rejected with an invalidArguments error.
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Limit uint `json:"limit,omitempty"`
|
||||
|
||||
// Does the client wish to know the total number of results in the query?
|
||||
//
|
||||
@@ -1052,7 +1056,7 @@ type EmailGetCommand struct {
|
||||
//
|
||||
// There is no requirement for the truncated form to be a balanced tree or valid HTML (indeed, the original
|
||||
// source may well be neither of these things).
|
||||
MaxBodyValueBytes int `json:"maxBodyValueBytes,omitempty"`
|
||||
MaxBodyValueBytes uint `json:"maxBodyValueBytes,omitempty"`
|
||||
}
|
||||
|
||||
// Reference to Previous Method Results
|
||||
@@ -1158,7 +1162,7 @@ type EmailGetRefCommand struct {
|
||||
//
|
||||
// There is no requirement for the truncated form to be a balanced tree or valid HTML (indeed, the original
|
||||
// source may well be neither of these things).
|
||||
MaxBodyValueBytes int `json:"maxBodyValueBytes,omitempty"`
|
||||
MaxBodyValueBytes uint `json:"maxBodyValueBytes,omitempty"`
|
||||
}
|
||||
|
||||
type EmailChangesCommand struct {
|
||||
@@ -1176,7 +1180,7 @@ type EmailChangesCommand struct {
|
||||
// The server MAY choose to return fewer than this value but MUST NOT return more.
|
||||
// If not given by the client, the server may choose how many to return.
|
||||
// If supplied by the client, the value MUST be a positive integer greater than 0.
|
||||
MaxChanges int `json:"maxChanges,omitzero"`
|
||||
MaxChanges uint `json:"maxChanges,omitzero"`
|
||||
}
|
||||
|
||||
type EmailAddress struct {
|
||||
@@ -1762,7 +1766,7 @@ type EmailSubmissionGetResponse struct {
|
||||
// When a client receives a response with a different state string to a previous call,
|
||||
// it MUST either throw away all currently cached objects for the type or call
|
||||
// EmailSubmission/changes to get the exact changes.
|
||||
State string `json:"state"`
|
||||
State State `json:"state"`
|
||||
|
||||
// An array of the EmailSubmission objects requested.
|
||||
//
|
||||
@@ -1815,8 +1819,8 @@ type EmailSubmissionCreate struct {
|
||||
type EmailSubmissionSetCommand struct {
|
||||
AccountId string `json:"accountId"`
|
||||
Create map[string]EmailSubmissionCreate `json:"create,omitempty"`
|
||||
OldState string `json:"oldState,omitempty"`
|
||||
NewState string `json:"newState,omitempty"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState,omitempty"`
|
||||
|
||||
// A map of EmailSubmission id to an object containing properties to update on the Email object
|
||||
// referenced by the EmailSubmission if the create/update/destroy succeeds.
|
||||
@@ -1842,10 +1846,10 @@ type EmailSubmissionSetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// This is the sinceState argument echoed back; it’s the state from which the server is returning changes.
|
||||
OldState string `json:"oldState"`
|
||||
OldState State `json:"oldState"`
|
||||
|
||||
// This is the state the client will be in after applying the set of changes to the old state.
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// If true, the client may call EmailSubmission/changes again with the newState returned to get further
|
||||
// updates.
|
||||
@@ -1930,7 +1934,7 @@ type Response struct {
|
||||
// Clients may use this to detect if this object has changed and needs to be refetched.
|
||||
//
|
||||
// [Section 2]: https://jmap.io/spec-core.html#the-jmap-session-resource
|
||||
SessionState string `json:"sessionState"`
|
||||
SessionState SessionState `json:"sessionState"`
|
||||
}
|
||||
|
||||
type EmailQueryResponse struct {
|
||||
@@ -1953,7 +1957,7 @@ type EmailQueryResponse struct {
|
||||
// Should a client receive back a response with a different queryState string to a previous call, it MUST either throw away the currently
|
||||
// cached query and fetch it again (note, this does not require fetching the records again, just the list of ids) or call
|
||||
// Email/queryChanges to get the difference.
|
||||
QueryState string `json:"queryState"`
|
||||
QueryState State `json:"queryState"`
|
||||
|
||||
// This is true if the server supports calling Email/queryChanges with these filter/sort parameters.
|
||||
//
|
||||
@@ -1962,7 +1966,7 @@ type EmailQueryResponse struct {
|
||||
CanCalculateChanges bool `json:"canCalculateChanges"`
|
||||
|
||||
// The zero-based index of the first result in the ids array within the complete list of query results.
|
||||
Position int `json:"position"`
|
||||
Position uint `json:"position"`
|
||||
|
||||
// The list of ids for each Email in the query results, starting at the index given by the position argument of this
|
||||
// response and continuing until it hits the end of the results or reaches the limit number of ids.
|
||||
@@ -1975,12 +1979,12 @@ type EmailQueryResponse struct {
|
||||
// Only if requested.
|
||||
//
|
||||
// This argument MUST be omitted if the calculateTotal request argument is not true.
|
||||
Total int `json:"total,omitempty,omitzero"`
|
||||
Total uint `json:"total,omitempty,omitzero"`
|
||||
|
||||
// The limit enforced by the server on the maximum number of results to return (if set by the server).
|
||||
//
|
||||
// This is only returned if the server set a limit or used a different limit than that given in the request.
|
||||
Limit int `json:"limit,omitempty,omitzero"`
|
||||
Limit uint `json:"limit,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
type EmailGetResponse struct {
|
||||
@@ -1992,7 +1996,7 @@ type EmailGetResponse struct {
|
||||
//
|
||||
// If the data changes, this string MUST change.
|
||||
// If the Email data is unchanged, servers SHOULD return the same state string on subsequent requests for this data type.
|
||||
State string `json:"state"`
|
||||
State State `json:"state"`
|
||||
|
||||
// An array of the Email objects requested.
|
||||
//
|
||||
@@ -2015,10 +2019,10 @@ type EmailChangesResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// This is the sinceState argument echoed back; it’s the state from which the server is returning changes.
|
||||
OldState string `json:"oldState"`
|
||||
OldState State `json:"oldState"`
|
||||
|
||||
// This is the state the client will be in after applying the set of changes to the old state.
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// If true, the client may call Email/changes again with the newState returned to get further updates.
|
||||
// If false, newState is the current server state.
|
||||
@@ -2044,7 +2048,7 @@ type MailboxGetResponse struct {
|
||||
// If the Mailbox data is unchanged, servers SHOULD return the same state string on subsequent requests for this data type.
|
||||
// When a client receives a response with a different state string to a previous call, it MUST either throw away all currently
|
||||
// cached objects for the type or call Foo/changes to get the exact changes.
|
||||
State string `json:"state"`
|
||||
State State `json:"state"`
|
||||
|
||||
// An array of the Mailbox objects requested.
|
||||
// This is the empty array if no objects were found or if the ids argument passed in was also an empty array.
|
||||
@@ -2063,10 +2067,10 @@ type MailboxChangesResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// This is the sinceState argument echoed back; it’s the state from which the server is returning changes.
|
||||
OldState string `json:"oldState"`
|
||||
OldState State `json:"oldState"`
|
||||
|
||||
// This is the state the client will be in after applying the set of changes to the old state.
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// If true, the client may call Mailbox/changes again with the newState returned to get further updates.
|
||||
//
|
||||
@@ -2108,7 +2112,7 @@ type MailboxQueryResponse struct {
|
||||
// Should a client receive back a response with a different queryState string to a previous call, it MUST either
|
||||
// throw away the currently cached query and fetch it again (note, this does not require fetching the records
|
||||
// again, just the list of ids) or call Mailbox/queryChanges to get the difference.
|
||||
QueryState string `json:"queryState"`
|
||||
QueryState State `json:"queryState"`
|
||||
|
||||
// This is true if the server supports calling Mailbox/queryChanges with these filter/sort parameters.
|
||||
//
|
||||
@@ -2212,10 +2216,10 @@ type EmailSetResponse struct {
|
||||
// The state string that would have been returned by Email/get before making the
|
||||
// requested changes, or null if the server doesn’t know what the previous state
|
||||
// string was.
|
||||
OldState string `json:"oldState,omitempty"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
|
||||
// The state string that will now be returned by Email/get.
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// A map of the creation id to an object containing any properties of the created Email object
|
||||
// that were not sent by the client.
|
||||
@@ -2313,10 +2317,10 @@ type EmailImportResponse struct {
|
||||
// The state string that would have been returned by Email/get on this account
|
||||
// before making the requested changes, or null if the server doesn’t know
|
||||
// what the previous state string was.
|
||||
OldState string `json:"oldState"`
|
||||
OldState State `json:"oldState"`
|
||||
|
||||
// The state string that will now be returned by Email/get on this account.
|
||||
NewState string `json:"newState"`
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// A map of the creation id to an object containing the id, blobId, threadId,
|
||||
// and size properties for each successfully imported Email, or null if none.
|
||||
@@ -2351,7 +2355,7 @@ type ThreadGetCommand struct {
|
||||
|
||||
type ThreadGetResponse struct {
|
||||
AccountId string
|
||||
State string
|
||||
State State
|
||||
List []Thread
|
||||
NotFound []any
|
||||
}
|
||||
@@ -2406,7 +2410,7 @@ type Identity struct {
|
||||
|
||||
type IdentityGetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
State string `json:"state"`
|
||||
State State `json:"state"`
|
||||
List []Identity `json:"list,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
@@ -2467,7 +2471,7 @@ type VacationResponseGetResponse struct {
|
||||
//
|
||||
// If the data changes, this string MUST change. If the data is unchanged, servers SHOULD return the same state string
|
||||
// on subsequent requests for this data type.
|
||||
State string `json:"state,omitempty"`
|
||||
State State `json:"state,omitempty"`
|
||||
|
||||
// An array of VacationResponse objects.
|
||||
List []VacationResponse `json:"list,omitempty"`
|
||||
@@ -2486,15 +2490,15 @@ type VacationResponseSetCommand struct {
|
||||
|
||||
type VacationResponseSetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
OldState string `json:"oldState,omitempty"`
|
||||
NewState string `json:"newState,omitempty"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState,omitempty"`
|
||||
Created map[string]VacationResponse `json:"created,omitempty"`
|
||||
Updated map[string]VacationResponse `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
NotCreated map[string]SetError `json:"notCreated,omitempty"`
|
||||
NotUpdated map[string]SetError `json:"notUpdated,omitempty"`
|
||||
NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
XXXXXXState State `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// One of these attributes must be set, but not both.
|
||||
@@ -2593,7 +2597,7 @@ func (b *Blob) Digest() string {
|
||||
|
||||
type BlobGetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
State string `json:"state,omitempty"`
|
||||
State State `json:"state,omitempty"`
|
||||
List []Blob `json:"list,omitempty"`
|
||||
NotFound []any `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type SessionEventListener interface {
|
||||
OnSessionOutdated(session *Session, newSessionState string)
|
||||
OnSessionOutdated(session *Session, newSessionState SessionState)
|
||||
}
|
||||
|
||||
// Cached user related information
|
||||
@@ -115,5 +115,5 @@ func (s Session) DecorateLogger(l log.Logger) *log.Logger {
|
||||
return log.From(l.With().
|
||||
Str(logUsername, s.Username).
|
||||
Str(logApiUrl, s.ApiUrl).
|
||||
Str(logSessionState, s.State))
|
||||
Str(logSessionState, string(s.State)))
|
||||
}
|
||||
|
||||
@@ -151,13 +151,15 @@ func TestRequests(t *testing.T) {
|
||||
|
||||
session := Session{Username: "user123", JmapUrl: *jmapUrl}
|
||||
|
||||
folders, err := client.GetAllMailboxes("a", &session, ctx, &logger)
|
||||
folders, sessionState, err := client.GetAllMailboxes("a", &session, ctx, &logger)
|
||||
require.NoError(err)
|
||||
require.Len(folders.Mailboxes, 5)
|
||||
require.NotEmpty(sessionState)
|
||||
|
||||
emails, err := client.GetAllEmails("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0)
|
||||
emails, sessionState, err := client.GetAllEmails("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0)
|
||||
require.NoError(err)
|
||||
require.Len(emails.Emails, 3)
|
||||
require.NotEmpty(sessionState)
|
||||
|
||||
{
|
||||
email := emails.Emails[0]
|
||||
|
||||
@@ -42,14 +42,14 @@ func command[T any](api ApiClient,
|
||||
logger *log.Logger,
|
||||
ctx context.Context,
|
||||
session *Session,
|
||||
sessionOutdatedHandler func(session *Session, newState string),
|
||||
sessionOutdatedHandler func(session *Session, newState SessionState),
|
||||
request Request,
|
||||
mapper func(body *Response) (T, Error)) (T, Error) {
|
||||
mapper func(body *Response) (T, Error)) (T, SessionState, Error) {
|
||||
|
||||
responseBody, jmapErr := api.Command(ctx, logger, session, request)
|
||||
if jmapErr != nil {
|
||||
var zero T
|
||||
return zero, jmapErr
|
||||
return zero, "", jmapErr
|
||||
}
|
||||
|
||||
var response Response
|
||||
@@ -57,7 +57,7 @@ func command[T any](api ApiClient,
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to deserialize body JSON payload")
|
||||
var zero T
|
||||
return zero, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
return zero, "", SimpleError{code: JmapErrorDecodingResponseBody, err: err}
|
||||
}
|
||||
|
||||
if response.SessionState != session.State {
|
||||
@@ -77,11 +77,13 @@ func command[T any](api ApiClient,
|
||||
}
|
||||
}
|
||||
var zero T
|
||||
return zero, SimpleError{code: JmapErrorMethodLevel, err: err}
|
||||
return zero, response.SessionState, SimpleError{code: JmapErrorMethodLevel, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
return mapper(&response)
|
||||
result, jerr := mapper(&response)
|
||||
sessionState := response.SessionState
|
||||
return result, sessionState, jerr
|
||||
}
|
||||
|
||||
func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
|
||||
|
||||
@@ -38,6 +38,22 @@ func SafeStringArray(array []string) SafeLogStringArrayMarshaller {
|
||||
return SafeLogStringArrayMarshaller{array: array}
|
||||
}
|
||||
|
||||
type StringArrayMarshaller struct {
|
||||
array []string
|
||||
}
|
||||
|
||||
func (m StringArrayMarshaller) MarshalZerologArray(a *zerolog.Array) {
|
||||
for _, elem := range m.array {
|
||||
a.Str(elem)
|
||||
}
|
||||
}
|
||||
|
||||
var _ zerolog.LogArrayMarshaler = StringArrayMarshaller{}
|
||||
|
||||
func StringArray(array []string) StringArrayMarshaller {
|
||||
return StringArrayMarshaller{array: array}
|
||||
}
|
||||
|
||||
func From(context zerolog.Context) *Logger {
|
||||
return &Logger{Logger: context.Logger()}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ type Mail struct {
|
||||
Master MailMasterAuth `yaml:"master"`
|
||||
BaseUrl string `yaml:"base_url" env:"GROUPWARE_JMAP_BASE_URL"`
|
||||
Timeout time.Duration `yaml:"timeout" env:"GROUPWARE_JMAP_TIMEOUT"`
|
||||
DefaultEmailLimit int `yaml:"default_email_limit" env:"GROUPWARE_DEFAULT_EMAIL_LIMIT"`
|
||||
MaxBodyValueBytes int `yaml:"max_body_value_bytes" env:"GROUPWARE_MAX_BODY_VALUE_BYTES"`
|
||||
DefaultEmailLimit uint `yaml:"default_email_limit" env:"GROUPWARE_DEFAULT_EMAIL_LIMIT"`
|
||||
MaxBodyValueBytes uint `yaml:"max_body_value_bytes" env:"GROUPWARE_MAX_BODY_VALUE_BYTES"`
|
||||
ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout" env:"GROUPWARE_RESPONSE_HEADER_TIMEOUT"`
|
||||
SessionCache MailSessionCache `yaml:"session_cache"`
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ func DefaultConfig() *config.Config {
|
||||
},
|
||||
BaseUrl: "https://stalwart.opencloud.test",
|
||||
Timeout: 30 * time.Second,
|
||||
DefaultEmailLimit: -1,
|
||||
MaxBodyValueBytes: -1,
|
||||
DefaultEmailLimit: uint(0),
|
||||
MaxBodyValueBytes: uint(0),
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
SessionCache: config.MailSessionCache{
|
||||
Ttl: 5 * time.Minute,
|
||||
|
||||
@@ -11,3 +11,9 @@ const (
|
||||
var Capabilities = []string{
|
||||
CapMail_1,
|
||||
}
|
||||
|
||||
const (
|
||||
RelationEntityEmail = "email"
|
||||
RelationTypeSameThread = "same-thread"
|
||||
RelationTypeSameSender = "same-sender"
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request)
|
||||
mailAccountId := req.GetAccountId()
|
||||
accountIds := structs.Keys(req.session.Accounts)
|
||||
|
||||
resp, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, req.logger)
|
||||
resp, sessionState, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, req.logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -73,6 +73,6 @@ func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request)
|
||||
Mailboxes: map[string][]jmap.Mailbox{
|
||||
mailAccountId: resp.Mailboxes,
|
||||
},
|
||||
}, resp.SessionState)
|
||||
}, sessionState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,7 +26,7 @@ func (g *Groupware) GetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
}
|
||||
|
||||
res, err := g.jmap.GetBlob(req.GetAccountId(), req.session, req.ctx, req.logger, blobId)
|
||||
res, _, err := g.jmap.GetBlob(req.GetAccountId(), req.session, req.ctx, req.logger, blobId)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func (g *Groupware) GetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
if blob == nil {
|
||||
return notFoundResponse("")
|
||||
}
|
||||
return etagOnlyResponse(res, blob.Digest())
|
||||
return etagOnlyResponse(res, jmap.State(blob.Digest()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
|
||||
return etagOnlyResponse(resp, resp.Sha512)
|
||||
return etagOnlyResponse(resp, jmap.State(resp.Sha512))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ type SwaggerGetIdentitiesResponse struct {
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, err := g.jmap.GetIdentity(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
res, sessionState, err := g.jmap.GetIdentity(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return response(res, res.State)
|
||||
return etagResponse(res, sessionState, res.State)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,11 +148,11 @@ type SwaggerIndexResponse struct {
|
||||
// responses:
|
||||
//
|
||||
// 200: IndexResponse
|
||||
func (g Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountIds := structs.Keys(req.session.Accounts)
|
||||
|
||||
identitiesResponse, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger)
|
||||
identitiesResponse, sessionState, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (g Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
Limits: buildIndexLimits(req.session),
|
||||
Accounts: buildIndexAccount(req.session, identitiesResponse.Identities),
|
||||
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
|
||||
}, req.session.State)
|
||||
}, sessionState)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,15 +39,15 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, err := g.jmap.GetMailbox(req.GetAccountId(), req.session, req.ctx, req.logger, []string{mailboxId})
|
||||
res, sessionState, err := g.jmap.GetMailbox(req.GetAccountId(), req.session, req.ctx, req.logger, []string{mailboxId})
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
|
||||
if len(res.Mailboxes) == 1 {
|
||||
return etagResponse(res.Mailboxes[0], res.SessionState, res.State)
|
||||
return etagResponse(res.Mailboxes[0], sessionState, res.State)
|
||||
} else {
|
||||
return notFoundResponse(res.SessionState)
|
||||
return notFoundResponse(sessionState)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -116,17 +116,17 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
if hasCriteria {
|
||||
mailboxes, err := g.jmap.SearchMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger, filter)
|
||||
mailboxes, sessionState, err := g.jmap.SearchMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger, filter)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return etagResponse(mailboxes.Mailboxes, mailboxes.SessionState, mailboxes.State)
|
||||
return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
|
||||
} else {
|
||||
mailboxes, err := g.jmap.GetAllMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
mailboxes, sessionState, err := g.jmap.GetAllMailboxes(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return etagResponse(mailboxes.Mailboxes, mailboxes.SessionState, mailboxes.State)
|
||||
return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
if since != "" {
|
||||
// ... then it's a completely different operation
|
||||
maxChanges := -1
|
||||
maxChanges := uint(0)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
if mailboxId == "" {
|
||||
errorId := req.errorId()
|
||||
@@ -70,12 +70,12 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(HeaderSince, since))
|
||||
|
||||
emails, jerr := g.jmap.GetEmailsInMailboxSince(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsInMailboxSince(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(emails, emails.State)
|
||||
return etagResponse(emails, sessionState, emails.State)
|
||||
})
|
||||
} else {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
@@ -88,30 +88,30 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
|
||||
withSource(&ErrorSource{Parameter: UriParamMailboxId}),
|
||||
))
|
||||
}
|
||||
offset, ok, err := req.parseNumericParam(QueryParamOffset, 0)
|
||||
offset, ok, err := req.parseUNumericParam(QueryParamOffset, 0)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamOffset, offset)
|
||||
l = l.Uint(QueryParamOffset, offset)
|
||||
}
|
||||
|
||||
limit, ok, err := req.parseNumericParam(QueryParamLimit, g.defaultEmailLimit)
|
||||
limit, ok, err := req.parseUNumericParam(QueryParamLimit, g.defaultEmailLimit)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamLimit, limit)
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
emails, jerr := g.jmap.GetAllEmails(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
emails, sessionState, jerr := g.jmap.GetAllEmails(req.GetAccountId(), req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(emails, emails.State)
|
||||
return etagResponse(emails, sessionState, emails.State)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -130,41 +130,41 @@ func (g *Groupware) GetMessagesById(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str("id", log.SafeString(id)))
|
||||
emails, jerr := g.jmap.GetEmails(req.GetAccountId(), req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes)
|
||||
emails, sessionState, jerr := g.jmap.GetEmails(req.GetAccountId(), req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(emails, emails.State)
|
||||
return etagResponse(emails, sessionState, emails.State)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groupware) getMessagesSince(w http.ResponseWriter, r *http.Request, since string) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With().Str(QueryParamSince, since)
|
||||
maxChanges, ok, err := req.parseNumericParam(QueryParamMaxChanges, -1)
|
||||
maxChanges, ok, err := req.parseUNumericParam(QueryParamMaxChanges, 0)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamMaxChanges, maxChanges)
|
||||
l = l.Uint(QueryParamMaxChanges, maxChanges)
|
||||
}
|
||||
logger := log.From(l)
|
||||
|
||||
emails, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
emails, sessionState, jerr := g.jmap.GetEmailsSince(req.GetAccountId(), req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(emails, emails.State)
|
||||
return etagResponse(emails, sessionState, emails.State)
|
||||
})
|
||||
}
|
||||
|
||||
type MessageSearchSnippetsResults struct {
|
||||
Results []jmap.SearchSnippet `json:"results,omitempty"`
|
||||
Total int `json:"total,omitzero"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
QueryState string `json:"queryState,omitempty"`
|
||||
Total uint `json:"total,omitzero"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
QueryState jmap.State `json:"queryState,omitempty"`
|
||||
}
|
||||
|
||||
type EmailWithSnippets struct {
|
||||
@@ -179,12 +179,12 @@ type SnippetWithoutEmailId struct {
|
||||
|
||||
type MessageSearchResults struct {
|
||||
Results []EmailWithSnippets `json:"results"`
|
||||
Total int `json:"total,omitzero"`
|
||||
Limit int `json:"limit,omitzero"`
|
||||
QueryState string `json:"queryState,omitempty"`
|
||||
Total uint `json:"total,omitzero"`
|
||||
Limit uint `json:"limit,omitzero"`
|
||||
QueryState jmap.State `json:"queryState,omitempty"`
|
||||
}
|
||||
|
||||
func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, int, int, *log.Logger, Response) {
|
||||
func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uint, uint, *log.Logger, Response) {
|
||||
q := req.r.URL.Query()
|
||||
mailboxId := q.Get(QueryParamMailboxId)
|
||||
notInMailboxIds := q[QueryParamNotInMailboxId]
|
||||
@@ -199,20 +199,20 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, int
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
offset, ok, err := req.parseNumericParam(QueryParamOffset, 0)
|
||||
offset, ok, err := req.parseUNumericParam(QueryParamOffset, 0)
|
||||
if err != nil {
|
||||
return false, nil, 0, 0, nil, errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamOffset, offset)
|
||||
l = l.Uint(QueryParamOffset, offset)
|
||||
}
|
||||
|
||||
limit, ok, err := req.parseNumericParam(QueryParamLimit, g.defaultEmailLimit)
|
||||
limit, ok, err := req.parseUNumericParam(QueryParamLimit, g.defaultEmailLimit)
|
||||
if err != nil {
|
||||
return false, nil, 0, 0, nil, errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamLimit, limit)
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
before, ok, err := req.parseDateParam(QueryParamSearchBefore)
|
||||
@@ -342,7 +342,7 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
logger = log.From(logger.With().Bool(QueryParamSearchFetchBodies, fetchBodies))
|
||||
}
|
||||
|
||||
results, jerr := g.jmap.QueryEmailsWithSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes)
|
||||
results, sessionState, jerr := g.jmap.QueryEmailsWithSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -367,9 +367,9 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
Total: results.Total,
|
||||
Limit: results.Limit,
|
||||
QueryState: results.QueryState,
|
||||
}, results.SessionState, results.QueryState)
|
||||
}, sessionState, results.QueryState)
|
||||
} else {
|
||||
results, jerr := g.jmap.QueryEmailSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit)
|
||||
results, sessionState, jerr := g.jmap.QueryEmailSnippets(req.GetAccountId(), filter, req.session, req.ctx, logger, offset, limit)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -379,7 +379,7 @@ func (g *Groupware) searchMessages(w http.ResponseWriter, r *http.Request) {
|
||||
Total: results.Total,
|
||||
Limit: results.Limit,
|
||||
QueryState: results.QueryState,
|
||||
}, results.SessionState, results.QueryState)
|
||||
}, sessionState, results.QueryState)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -441,12 +441,12 @@ func (g *Groupware) CreateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
BodyValues: body.BodyValues,
|
||||
}
|
||||
|
||||
created, jerr := g.jmap.CreateEmail(req.GetAccountId(), create, req.session, req.ctx, logger)
|
||||
created, sessionState, jerr := g.jmap.CreateEmail(req.GetAccountId(), create, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(created.Email, created.SessionState)
|
||||
return response(created.Email, sessionState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
messageId: body,
|
||||
}
|
||||
|
||||
result, jerr := g.jmap.UpdateEmails(req.GetAccountId(), updates, req.session, req.ctx, logger)
|
||||
result, sessionState, jerr := g.jmap.UpdateEmails(req.GetAccountId(), updates, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
@@ -484,7 +484,7 @@ func (g *Groupware) UpdateMessage(w http.ResponseWriter, r *http.Request) {
|
||||
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
|
||||
}
|
||||
|
||||
return response(updatedEmail, result.SessionState)
|
||||
return response(updatedEmail, sessionState)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -498,12 +498,12 @@ func (g *Groupware) DeleteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
deleted, jerr := g.jmap.DeleteEmails(req.GetAccountId(), []string{messageId}, req.session, req.ctx, logger)
|
||||
_, sessionState, jerr := g.jmap.DeleteEmails(req.GetAccountId(), []string{messageId}, req.session, req.ctx, logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return noContentResponse(deleted.SessionState)
|
||||
return noContentResponse(sessionState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ type AboutMessageResponse struct {
|
||||
// Key (AES-256)
|
||||
}
|
||||
|
||||
func relatedEmails(email jmap.Email, beacon time.Time, days int) jmap.EmailFilterElement {
|
||||
func relatedEmails(email jmap.Email, beacon time.Time, days uint) jmap.EmailFilterElement {
|
||||
filters := []jmap.EmailFilterElement{}
|
||||
for _, from := range email.From {
|
||||
if from.Email != "" {
|
||||
@@ -557,23 +557,30 @@ func relatedEmails(email jmap.Email, beacon time.Time, days int) jmap.EmailFilte
|
||||
return filter
|
||||
}
|
||||
|
||||
func (g *Groupware) AboutMessage(w http.ResponseWriter, r *http.Request) {
|
||||
func (g *Groupware) RelatedToMessage(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, UriParamMessageId)
|
||||
|
||||
limit := 10 // TODO configurable
|
||||
days := 3 // TODO configurable
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
limit, _, err := req.parseUNumericParam(QueryParamLimit, 10) // TODO configurable default limit
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
days, _, err := req.parseUNumericParam(QueryParamDays, 5) // TODO configurable default days
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
reqId := req.GetRequestId()
|
||||
accountId := req.GetAccountId()
|
||||
logger := log.From(req.logger.With().Str("id", log.SafeString(id)))
|
||||
emails, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, []string{id}, true, g.maxBodyValueBytes)
|
||||
logger := log.From(req.logger.With().Str(logEmailId, log.SafeString(id)))
|
||||
emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, []string{id}, true, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
if len(emails.Emails) < 1 {
|
||||
logger.Trace().Msg("failed to find any emails matching id")
|
||||
return notFoundResponse(emails.SessionState)
|
||||
logger.Trace().Msg("failed to find any emails matching id") // the id is already in the log field
|
||||
return notFoundResponse(sessionState)
|
||||
}
|
||||
email := emails.Emails[0]
|
||||
|
||||
@@ -584,32 +591,45 @@ func (g *Groupware) AboutMessage(w http.ResponseWriter, r *http.Request) {
|
||||
// bgctx, _ := context.WithTimeout(context.Background(), time.Duration(30)*time.Second) // TODO configurable
|
||||
bgctx := context.Background()
|
||||
|
||||
g.job(logger, "query related emails", func(jobId uint64, l *log.Logger) {
|
||||
results, jerr := g.jmap.QueryEmails(accountId, filter, req.session, bgctx, l, 0, limit, false, g.maxBodyValueBytes)
|
||||
g.job(logger, RelationTypeSameSender, func(jobId uint64, l *log.Logger) {
|
||||
results, _, jerr := g.jmap.QueryEmails(accountId, filter, req.session, bgctx, l, 0, limit, false, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
l.Error().Err(jerr)
|
||||
l.Error().Err(jerr).Msgf("failed to query %v emails", RelationTypeSameSender)
|
||||
} else {
|
||||
l.Trace().Msgf("about query found %v emails", len(results.Results))
|
||||
// TODO filter out the original email
|
||||
req.push("email", AboutMessageEmailsEvent{Id: reqId, Emails: results.Results, Source: "same-sender"})
|
||||
related := filterEmails(results.Emails, email)
|
||||
l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameSender, len(related))
|
||||
if len(related) > 0 {
|
||||
req.push(RelationEntityEmail, AboutMessageEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
g.job(logger, "emails in thread", func(jobId uint64, l *log.Logger) {
|
||||
results, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, false, g.maxBodyValueBytes)
|
||||
l.Info().Interface("results", results).Msg("emails in thread?")
|
||||
g.job(logger, RelationTypeSameThread, func(jobId uint64, l *log.Logger) {
|
||||
emails, _, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, false, g.maxBodyValueBytes)
|
||||
if jerr != nil {
|
||||
l.Error().Err(jerr)
|
||||
l.Error().Err(jerr).Msgf("failed to list %v emails", RelationTypeSameThread)
|
||||
} else {
|
||||
l.Trace().Msgf("about thread query found %v emails", len(results.Emails))
|
||||
// TODO filter out the original email
|
||||
req.push("email", AboutMessageEmailsEvent{Id: reqId, Emails: results.Emails, Source: "same-thread"})
|
||||
related := filterEmails(emails, email)
|
||||
l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameThread, len(related))
|
||||
if len(related) > 0 {
|
||||
req.push(RelationEntityEmail, AboutMessageEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameThread})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return response(AboutMessageResponse{
|
||||
return etagResponse(AboutMessageResponse{
|
||||
Email: email,
|
||||
RequestId: reqId,
|
||||
}, emails.State)
|
||||
}, sessionState, emails.State)
|
||||
})
|
||||
}
|
||||
|
||||
func filterEmails(all []jmap.Email, skip jmap.Email) []jmap.Email {
|
||||
filtered := all[:0]
|
||||
for _, email := range all {
|
||||
if skip.Id != email.Id {
|
||||
filtered = append(filtered, email)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ type SwaggerGetVacationResponse200 struct {
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
res, err := g.jmap.GetVacationResponse(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
res, sessionState, err := g.jmap.GetVacationResponse(req.GetAccountId(), req.session, req.ctx, req.logger)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(err)
|
||||
}
|
||||
return response(res, res.State)
|
||||
return etagResponse(res, sessionState, res.State)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,10 +66,11 @@ func (g *Groupware) SetVacation(w http.ResponseWriter, r *http.Request) {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
res, jerr := g.jmap.SetVacationResponse(req.GetAccountId(), body, req.session, req.ctx, req.logger)
|
||||
res, sessionState, jerr := g.jmap.SetVacationResponse(req.GetAccountId(), body, req.session, req.ctx, req.logger)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
return response(res, res.SessionState)
|
||||
|
||||
return etagResponse(res, sessionState, res.ResponseState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const (
|
||||
logInvalidPathParameter = "error-path-param"
|
||||
logFolderId = "folder-id"
|
||||
logQuery = "query"
|
||||
logEmailId = "email-id"
|
||||
)
|
||||
|
||||
type User interface {
|
||||
@@ -64,8 +65,8 @@ type Groupware struct {
|
||||
streams map[string]time.Time
|
||||
streamsLock sync.Mutex
|
||||
logger *log.Logger
|
||||
defaultEmailLimit int
|
||||
maxBodyValueBytes int
|
||||
defaultEmailLimit uint
|
||||
maxBodyValueBytes uint
|
||||
sessionCache *ttlcache.Cache[string, cachedSession]
|
||||
jmap *jmap.Client
|
||||
userProvider UserProvider
|
||||
@@ -95,12 +96,12 @@ type GroupwareSessionEventListener struct {
|
||||
sessionCache *ttlcache.Cache[string, cachedSession]
|
||||
}
|
||||
|
||||
func (l GroupwareSessionEventListener) OnSessionOutdated(session *jmap.Session, newSessionState string) {
|
||||
func (l GroupwareSessionEventListener) OnSessionOutdated(session *jmap.Session, newSessionState jmap.SessionState) {
|
||||
// it's enough to remove the session from the cache, as it will be fetched on-demand
|
||||
// the next time an operation is performed on behalf of the user
|
||||
l.sessionCache.Delete(session.Username)
|
||||
|
||||
l.logger.Trace().Msgf("removed outdated session for user '%v': state %s -> %s", session.Username, session.State, newSessionState)
|
||||
l.logger.Trace().Msgf("removed outdated session for user '%v': state %v -> %v", session.Username, session.State, newSessionState)
|
||||
}
|
||||
|
||||
var _ jmap.SessionEventListener = GroupwareSessionEventListener{}
|
||||
@@ -182,15 +183,16 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) (*Gro
|
||||
case ttlcache.EvictionReasonCapacityReached:
|
||||
reason = "capacity reached"
|
||||
case ttlcache.EvictionReasonExpired:
|
||||
reason = fmt.Sprintf("expired after %vms", item.TTL().Milliseconds())
|
||||
reason = fmt.Sprintf("expired after %v", item.TTL())
|
||||
case ttlcache.EvictionReasonMaxCostExceeded:
|
||||
reason = "max cost exceeded"
|
||||
}
|
||||
if reason == "" {
|
||||
reason = fmt.Sprintf("unknown (%v)", r)
|
||||
}
|
||||
spentInCache := time.Since(item.Value().Since())
|
||||
|
||||
logger.Trace().Msgf("session cache eviction of user '%v': %v", item.Key(), reason)
|
||||
logger.Trace().Msgf("session cache eviction of user '%v' after %v: %v", item.Key(), spentInCache, reason)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -222,15 +224,6 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) (*Gro
|
||||
jobCounter: atomic.Uint64{},
|
||||
}
|
||||
|
||||
/*
|
||||
sessionCache.OnInsertion(func(c context.Context, item *ttlcache.Item[string, cachedSession]) {
|
||||
str := sseServer.CreateStream(item.Key())
|
||||
if logger.Trace().Enabled() {
|
||||
logger.Trace().Msgf("created stream %v for '%v'", log.SafeString(str.ID), log.SafeString(item.Key()))
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
for w := 1; w <= workerPoolSize; w++ {
|
||||
go g.worker(jobsChannel)
|
||||
}
|
||||
@@ -364,8 +357,8 @@ type Response struct {
|
||||
body any
|
||||
status int
|
||||
err *Error
|
||||
etag string
|
||||
sessionState string
|
||||
etag jmap.State
|
||||
sessionState jmap.SessionState
|
||||
}
|
||||
|
||||
func errorResponse(err *Error) Response {
|
||||
@@ -377,16 +370,16 @@ func errorResponse(err *Error) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func response(body any, sessionStatus string) Response {
|
||||
func response(body any, sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
etag: sessionStatus,
|
||||
sessionState: sessionStatus,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func etagResponse(body any, sessionState string, etag string) Response {
|
||||
func etagResponse(body any, sessionState jmap.SessionState, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
@@ -395,7 +388,7 @@ func etagResponse(body any, sessionState string, etag string) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func etagOnlyResponse(body any, etag string) Response {
|
||||
func etagOnlyResponse(body any, etag jmap.State) Response {
|
||||
return Response{
|
||||
body: body,
|
||||
err: nil,
|
||||
@@ -404,43 +397,43 @@ func etagOnlyResponse(body any, etag string) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func noContentResponse(sessionStatus string) Response {
|
||||
func noContentResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNoContent,
|
||||
err: nil,
|
||||
etag: sessionStatus,
|
||||
sessionState: sessionStatus,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func acceptedResponse(sessionStatus string) Response {
|
||||
func acceptedResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusAccepted,
|
||||
err: nil,
|
||||
etag: sessionStatus,
|
||||
sessionState: sessionStatus,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func timeoutResponse(sessionStatus string) Response {
|
||||
func timeoutResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusRequestTimeout,
|
||||
err: nil,
|
||||
etag: "",
|
||||
sessionState: sessionStatus,
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func notFoundResponse(sessionStatus string) Response {
|
||||
func notFoundResponse(sessionState jmap.SessionState) Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNotFound,
|
||||
err: nil,
|
||||
etag: sessionStatus,
|
||||
sessionState: sessionStatus,
|
||||
etag: jmap.State(sessionState),
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +483,9 @@ func (r Request) parseNumericParam(param string, defaultValue int) (int, bool, *
|
||||
value, err := strconv.ParseInt(str, 10, 0)
|
||||
if err != nil {
|
||||
errorId := r.errorId()
|
||||
msg := fmt.Sprintf("Invalid value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
|
||||
// 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, apiError(errorId, ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: param}),
|
||||
@@ -499,6 +494,31 @@ func (r Request) parseNumericParam(param string, defaultValue int) (int, bool, *
|
||||
return int(value), true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseUNumericParam(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 {
|
||||
errorId := r.errorId()
|
||||
// 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, apiError(errorId, 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) {
|
||||
@@ -658,22 +678,42 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons
|
||||
return
|
||||
}
|
||||
|
||||
etag := ""
|
||||
sessionState := ""
|
||||
|
||||
if response.etag != "" {
|
||||
w.Header().Add("ETag", response.etag)
|
||||
}
|
||||
if response.sessionState != "" {
|
||||
if response.etag == "" {
|
||||
w.Header().Add("ETag", response.sessionState)
|
||||
}
|
||||
w.Header().Add("Session-State", response.sessionState)
|
||||
etag = string(response.etag)
|
||||
}
|
||||
|
||||
switch response.body {
|
||||
case nil, "":
|
||||
w.WriteHeader(response.status)
|
||||
default:
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, response.body)
|
||||
if response.sessionState != "" {
|
||||
sessionState = string(response.sessionState)
|
||||
if etag == "" {
|
||||
etag = sessionState
|
||||
}
|
||||
}
|
||||
|
||||
if sessionState != "" {
|
||||
w.Header().Add("Session-State", string(sessionState))
|
||||
}
|
||||
|
||||
notModified := false
|
||||
if etag != "" {
|
||||
challenge := r.Header.Get("if-none-match")
|
||||
quotedEtag := "\"" + etag + "\""
|
||||
notModified = challenge != "" && (challenge == etag || challenge == quotedEtag)
|
||||
w.Header().Add("ETag", quotedEtag)
|
||||
}
|
||||
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
} else {
|
||||
switch response.body {
|
||||
case nil, "":
|
||||
w.WriteHeader(response.status)
|
||||
default:
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, response.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const (
|
||||
QueryParamSearchFetchEmails = "fetchemails"
|
||||
QueryParamOffset = "offset"
|
||||
QueryParamLimit = "limit"
|
||||
QueryParamDays = "days"
|
||||
HeaderSince = "if-none-match"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,7 @@ func (g *Groupware) Route(r chi.Router) {
|
||||
// r.Put("/{messageid}", g.ReplaceMessage) // TODO
|
||||
r.Patch("/{messageid}", g.UpdateMessage)
|
||||
r.Delete("/{messageid}", g.DeleteMessage)
|
||||
r.MethodFunc("REPORT", "/{messageid}", g.AboutMessage)
|
||||
r.MethodFunc("REPORT", "/{messageid}", g.RelatedToMessage)
|
||||
})
|
||||
r.Route("/blobs", func(r chi.Router) {
|
||||
r.Get("/{blobid}", g.GetBlob)
|
||||
|
||||
@@ -12,9 +12,11 @@ type cachedSession interface {
|
||||
Success() bool
|
||||
Get() jmap.Session
|
||||
Error() error
|
||||
Since() time.Time
|
||||
}
|
||||
|
||||
type succeededSession struct {
|
||||
since time.Time
|
||||
session jmap.Session
|
||||
}
|
||||
|
||||
@@ -27,11 +29,15 @@ func (s succeededSession) Get() jmap.Session {
|
||||
func (s succeededSession) Error() error {
|
||||
return nil
|
||||
}
|
||||
func (s succeededSession) Since() time.Time {
|
||||
return s.since
|
||||
}
|
||||
|
||||
var _ cachedSession = succeededSession{}
|
||||
|
||||
type failedSession struct {
|
||||
err error
|
||||
since time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
func (s failedSession) Success() bool {
|
||||
@@ -43,6 +49,9 @@ func (s failedSession) Get() jmap.Session {
|
||||
func (s failedSession) Error() error {
|
||||
return s.err
|
||||
}
|
||||
func (s failedSession) Since() time.Time {
|
||||
return s.since
|
||||
}
|
||||
|
||||
var _ cachedSession = failedSession{}
|
||||
|
||||
|
||||
45
services/groupware/pkg/middleware/groupware_logger.go
Normal file
45
services/groupware/pkg/middleware/groupware_logger.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
requestID := middleware.GetReqID(ctx)
|
||||
traceID := GetTraceID(ctx)
|
||||
|
||||
level.Str(log.RequestIDString, requestID)
|
||||
|
||||
if traceID != "" {
|
||||
level.Str("traceId", traceID)
|
||||
}
|
||||
|
||||
level.
|
||||
Str("proto", r.Proto).
|
||||
Str("method", r.Method).
|
||||
Int("status", wrap.Status()).
|
||||
Str("path", r.URL.Path).
|
||||
Dur("duration", time.Since(start)).
|
||||
Int("bytes", wrap.BytesWritten()).
|
||||
Msg("")
|
||||
})
|
||||
}
|
||||
}
|
||||
42
services/groupware/pkg/middleware/traceid.go
Normal file
42
services/groupware/pkg/middleware/traceid.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ctxKeyTraceID int
|
||||
|
||||
const TraceIDKey ctxKeyTraceID = 0
|
||||
|
||||
const maxTraceIdLength = 1024
|
||||
|
||||
var TraceIDHeader = "Trace-Id"
|
||||
|
||||
func TraceID(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
traceID := r.Header.Get(TraceIDHeader)
|
||||
if traceID != "" {
|
||||
runes := []rune(traceID)
|
||||
if len(runes) > maxTraceIdLength {
|
||||
traceID = string(runes[0:maxTraceIdLength])
|
||||
}
|
||||
w.Header().Add(TraceIDHeader, traceID)
|
||||
ctx := context.WithValue(r.Context(), TraceIDKey, traceID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func GetTraceID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if traceID, ok := ctx.Value(TraceIDKey).(string); ok {
|
||||
return traceID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -41,6 +41,7 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
svc.Middleware(
|
||||
middleware.RealIP,
|
||||
middleware.RequestID,
|
||||
groupwaremiddleware.TraceID,
|
||||
opencloudmiddleware.Cors(
|
||||
cors.Logger(options.Logger),
|
||||
cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
|
||||
@@ -52,7 +53,7 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
options.Config.Service.Name,
|
||||
version.GetString(),
|
||||
),
|
||||
opencloudmiddleware.Logger(options.Logger),
|
||||
groupwaremiddleware.GroupwareLogger(options.Logger),
|
||||
groupwaremiddleware.Auth(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/riandyrn/otelchi"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/tracing"
|
||||
"github.com/opencloud-eu/opencloud/services/groupware/pkg/groupware"
|
||||
)
|
||||
@@ -38,10 +40,17 @@ func NewService(opts ...Option) (Service, error) {
|
||||
|
||||
m.Route(options.Config.HTTP.Root, gw.Route)
|
||||
|
||||
_ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
{
|
||||
level := options.Logger.Debug()
|
||||
if level.Enabled() {
|
||||
routes := []string{}
|
||||
_ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
routes = append(routes, fmt.Sprintf("%s %s", method, route))
|
||||
return nil
|
||||
})
|
||||
level.Array("routes", log.StringArray(routes)).Msgf("serving %v endpoints", len(routes))
|
||||
}
|
||||
}
|
||||
|
||||
return gw, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user