diff --git a/pkg/jmap/jmap_api_blob.go b/pkg/jmap/jmap_api_blob.go index 2bb317f120..e992ab3eb6 100644 --- a/pkg/jmap/jmap_api_blob.go +++ b/pkg/jmap/jmap_api_blob.go @@ -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 }) diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index 9b2abdcacb..8b69a95cd2 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -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 }) } diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go index 479e1da321..1e283cd54a 100644 --- a/pkg/jmap/jmap_api_identity.go +++ b/pkg/jmap/jmap_api_identity.go @@ -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 }) } diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go index 257e644c8e..83a8e79d34 100644 --- a/pkg/jmap/jmap_api_mailbox.go +++ b/pkg/jmap/jmap_api_mailbox.go @@ -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 }) } diff --git a/pkg/jmap/jmap_api_vacation.go b/pkg/jmap/jmap_api_vacation.go index cd2867d2f8..af695d1f10 100644 --- a/pkg/jmap/jmap_api_vacation.go +++ b/pkg/jmap/jmap_api_vacation.go @@ -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 }) } diff --git a/pkg/jmap/jmap_client.go b/pkg/jmap/jmap_client.go index 2125e70df1..3b487c840f 100644 --- a/pkg/jmap/jmap_client.go +++ b/pkg/jmap/jmap_client.go @@ -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) }) diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index daaeecb115..eabd567d7a 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -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"` } diff --git a/pkg/jmap/jmap_session.go b/pkg/jmap/jmap_session.go index 26b627ea0e..0138943e36 100644 --- a/pkg/jmap/jmap_session.go +++ b/pkg/jmap/jmap_session.go @@ -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))) } diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go index df393436cb..e428b1415c 100644 --- a/pkg/jmap/jmap_test.go +++ b/pkg/jmap/jmap_test.go @@ -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] diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go index b604145f9c..34bb25b46b 100644 --- a/pkg/jmap/jmap_tools.go +++ b/pkg/jmap/jmap_tools.go @@ -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 { diff --git a/pkg/log/log_safely.go b/pkg/log/log_safely.go index fb134ac3ef..67c4df9dd6 100644 --- a/pkg/log/log_safely.go +++ b/pkg/log/log_safely.go @@ -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()} } diff --git a/services/groupware/pkg/config/config.go b/services/groupware/pkg/config/config.go index 0c66c7e172..bcd75e8a4d 100644 --- a/services/groupware/pkg/config/config.go +++ b/services/groupware/pkg/config/config.go @@ -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"` } diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go index a45a1a39c2..c9bae68aa2 100644 --- a/services/groupware/pkg/config/defaults/defaultconfig.go +++ b/services/groupware/pkg/config/defaults/defaultconfig.go @@ -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, diff --git a/services/groupware/pkg/groupware/groupware_api.go b/services/groupware/pkg/groupware/groupware_api.go index fa685dd8c7..44ed201447 100644 --- a/services/groupware/pkg/groupware/groupware_api.go +++ b/services/groupware/pkg/groupware/groupware_api.go @@ -11,3 +11,9 @@ const ( var Capabilities = []string{ CapMail_1, } + +const ( + RelationEntityEmail = "email" + RelationTypeSameThread = "same-thread" + RelationTypeSameSender = "same-sender" +) diff --git a/services/groupware/pkg/groupware/groupware_api_account.go b/services/groupware/pkg/groupware/groupware_api_account.go index 946718a763..341fc56912 100644 --- a/services/groupware/pkg/groupware/groupware_api_account.go +++ b/services/groupware/pkg/groupware/groupware_api_account.go @@ -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) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_blob.go b/services/groupware/pkg/groupware/groupware_api_blob.go index 113e0ba5a1..d0647459f8 100644 --- a/services/groupware/pkg/groupware/groupware_api_blob.go +++ b/services/groupware/pkg/groupware/groupware_api_blob.go @@ -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)) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go index 6a7a993534..0b4cebc3a9 100644 --- a/services/groupware/pkg/groupware/groupware_api_identity.go +++ b/services/groupware/pkg/groupware/groupware_api_identity.go @@ -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) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_index.go b/services/groupware/pkg/groupware/groupware_api_index.go index 25be9ea25b..8748021778 100644 --- a/services/groupware/pkg/groupware/groupware_api_index.go +++ b/services/groupware/pkg/groupware/groupware_api_index.go @@ -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) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go index 2cf219e20b..5d6bdf6019 100644 --- a/services/groupware/pkg/groupware/groupware_api_mailbox.go +++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go @@ -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) } }) } diff --git a/services/groupware/pkg/groupware/groupware_api_messages.go b/services/groupware/pkg/groupware/groupware_api_messages.go index 83bbc2a6a6..d4dd8719cc 100644 --- a/services/groupware/pkg/groupware/groupware_api_messages.go +++ b/services/groupware/pkg/groupware/groupware_api_messages.go @@ -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 +} diff --git a/services/groupware/pkg/groupware/groupware_api_vacation.go b/services/groupware/pkg/groupware/groupware_api_vacation.go index 0d32307efa..9d26060d9c 100644 --- a/services/groupware/pkg/groupware/groupware_api_vacation.go +++ b/services/groupware/pkg/groupware/groupware_api_vacation.go @@ -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) }) } diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index a6f6567654..bb9cf13f3b 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -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) + } } } diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index bd98511d5e..f763f86e25 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -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) diff --git a/services/groupware/pkg/groupware/groupware_session.go b/services/groupware/pkg/groupware/groupware_session.go index e7e27d6237..19a6a1d0da 100644 --- a/services/groupware/pkg/groupware/groupware_session.go +++ b/services/groupware/pkg/groupware/groupware_session.go @@ -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{} diff --git a/services/groupware/pkg/middleware/groupware_logger.go b/services/groupware/pkg/middleware/groupware_logger.go new file mode 100644 index 0000000000..b7e265908b --- /dev/null +++ b/services/groupware/pkg/middleware/groupware_logger.go @@ -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("") + }) + } +} diff --git a/services/groupware/pkg/middleware/traceid.go b/services/groupware/pkg/middleware/traceid.go new file mode 100644 index 0000000000..a71ea3236f --- /dev/null +++ b/services/groupware/pkg/middleware/traceid.go @@ -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 "" +} diff --git a/services/groupware/pkg/server/http/server.go b/services/groupware/pkg/server/http/server.go index 5c5320ee70..90ebb81f5a 100644 --- a/services/groupware/pkg/server/http/server.go +++ b/services/groupware/pkg/server/http/server.go @@ -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), diff --git a/services/groupware/pkg/service/http/v0/service.go b/services/groupware/pkg/service/http/v0/service.go index d0f4f25341..2eea65bf17 100644 --- a/services/groupware/pkg/service/http/v0/service.go +++ b/services/groupware/pkg/service/http/v0/service.go @@ -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 }