From deee6102ec22684cdfae58771aa586b323604c5e Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Mon, 6 Oct 2025 11:58:36 +0200 Subject: [PATCH] groupware: add quota API + add support for Accept-Language and Content-Language --- pkg/jmap/jmap_api.go | 6 +- pkg/jmap/jmap_api_blob.go | 20 ++--- pkg/jmap/jmap_api_email.go | 78 +++++++++---------- pkg/jmap/jmap_api_identity.go | 18 ++--- pkg/jmap/jmap_api_mailbox.go | 42 +++++----- pkg/jmap/jmap_api_quota.go | 23 ++++++ pkg/jmap/jmap_api_vacation.go | 12 +-- pkg/jmap/jmap_http.go | 56 ++++++++----- pkg/jmap/jmap_integration_test.go | 8 +- pkg/jmap/jmap_model.go | 16 ++++ pkg/jmap/jmap_test.go | 26 +++---- pkg/jmap/jmap_tools.go | 15 ++-- pkg/jmap/jmap_tools_test.go | 4 +- services/groupware/apidoc.yml | 6 ++ .../pkg/groupware/groupware_api_account.go | 8 +- .../pkg/groupware/groupware_api_blob.go | 15 ++-- .../pkg/groupware/groupware_api_calendars.go | 6 +- .../pkg/groupware/groupware_api_contacts.go | 6 +- .../pkg/groupware/groupware_api_emails.go | 78 ++++++++++--------- .../pkg/groupware/groupware_api_identity.go | 4 +- .../pkg/groupware/groupware_api_index.go | 4 +- .../pkg/groupware/groupware_api_mailbox.go | 36 ++++----- .../pkg/groupware/groupware_api_quota.go | 39 ++++++++++ .../pkg/groupware/groupware_api_tasklists.go | 6 +- .../pkg/groupware/groupware_api_vacation.go | 8 +- .../pkg/groupware/groupware_framework.go | 4 + .../pkg/groupware/groupware_request.go | 10 ++- .../pkg/groupware/groupware_response.go | 44 ++++++----- .../pkg/groupware/groupware_route.go | 1 + 29 files changed, 362 insertions(+), 237 deletions(-) create mode 100644 pkg/jmap/jmap_api_quota.go create mode 100644 services/groupware/pkg/groupware/groupware_api_quota.go diff --git a/pkg/jmap/jmap_api.go b/pkg/jmap/jmap_api.go index 24080ac47..d864d3087 100644 --- a/pkg/jmap/jmap_api.go +++ b/pkg/jmap/jmap_api.go @@ -9,7 +9,7 @@ import ( ) type ApiClient interface { - Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) + Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) io.Closer } @@ -33,8 +33,8 @@ type SessionClient interface { } type BlobClient interface { - UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, content io.Reader) (UploadedBlob, Error) - DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error) + UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, content io.Reader) (UploadedBlob, Language, Error) + DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) io.Closer } diff --git a/pkg/jmap/jmap_api_blob.go b/pkg/jmap/jmap_api_blob.go index 788a8ca00..2266c1d10 100644 --- a/pkg/jmap/jmap_api_blob.go +++ b/pkg/jmap/jmap_api_blob.go @@ -14,7 +14,7 @@ type BlobResponse struct { State State `json:"state,omitempty"` } -func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context.Context, logger *log.Logger, id string) (BlobResponse, SessionState, Error) { +func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (BlobResponse, SessionState, Language, Error) { cmd, jerr := j.request(session, logger, invocation(CommandBlobGet, BlobGetCommand{ AccountId: accountId, @@ -24,10 +24,10 @@ func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context }, "0"), ) if jerr != nil { - return BlobResponse{}, "", jerr + return BlobResponse{}, "", "", jerr } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (BlobResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (BlobResponse, Error) { var response BlobGetResponse err := retrieveResponseMatchParameters(logger, body, CommandBlobGet, "0", &response) if err != nil { @@ -51,14 +51,14 @@ type UploadedBlob struct { 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) { +func (j *Client) UploadBlobStream(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, contentType string, body io.Reader) (UploadedBlob, Language, Error) { logger = log.From(logger.With().Str(logEndpoint, session.UploadEndpoint)) // TODO(pbleser-oc) use a library for proper URL template parsing uploadUrl := strings.ReplaceAll(session.UploadUrlTemplate, "{accountId}", accountId) - return j.blob.UploadBinary(ctx, logger, session, uploadUrl, session.UploadEndpoint, contentType, body) + return j.blob.UploadBinary(ctx, logger, session, uploadUrl, session.UploadEndpoint, contentType, acceptLanguage, body) } -func (j *Client) DownloadBlobStream(accountId string, blobId string, name string, typ string, session *Session, ctx context.Context, logger *log.Logger) (*BlobDownload, Error) { +func (j *Client) DownloadBlobStream(accountId string, blobId string, name string, typ string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (*BlobDownload, Language, Error) { logger = log.From(logger.With().Str(logEndpoint, session.DownloadEndpoint)) // TODO(pbleser-oc) use a library for proper URL template parsing downloadUrl := session.DownloadUrlTemplate @@ -67,10 +67,10 @@ func (j *Client) DownloadBlobStream(accountId string, blobId string, name string downloadUrl = strings.ReplaceAll(downloadUrl, "{name}", name) downloadUrl = strings.ReplaceAll(downloadUrl, "{type}", typ) logger = log.From(logger.With().Str(logDownloadUrl, downloadUrl).Str(logBlobId, blobId)) - return j.blob.DownloadBinary(ctx, logger, session, downloadUrl, session.DownloadEndpoint) + return j.blob.DownloadBinary(ctx, logger, session, downloadUrl, session.DownloadEndpoint, acceptLanguage) } -func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte, contentType string) (UploadedBlob, SessionState, Error) { +func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte, contentType string) (UploadedBlob, SessionState, Language, Error) { encoded := base64.StdEncoding.EncodeToString(data) upload := BlobUploadCommand{ @@ -100,10 +100,10 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont invocation(CommandBlobGet, getHash, "1"), ) if jerr != nil { - return UploadedBlob{}, "", jerr + return UploadedBlob{}, "", "", jerr } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (UploadedBlob, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UploadedBlob, Error) { var uploadResponse BlobUploadResponse err := retrieveResponseMatchParameters(logger, body, CommandBlobUpload, "0", &uploadResponse) if err != nil { diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index 0f42197a9..c44ae70dc 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -32,7 +32,7 @@ type Emails struct { } // Retrieve specific Emails by their id. -func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) { +func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Language, Error) { logger = j.logger("GetEmails", session, logger) get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies} @@ -43,9 +43,9 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0")) if err != nil { logger.Error().Err(err).Send() - 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) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, Error) { var response EmailGetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "0", &response) if err != nil { @@ -56,7 +56,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte } // Retrieve all the Emails in a given Mailbox by its id. -func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) { +func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Language, Error) { logger = j.loggerParams("GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Uint(logOffset, offset).Uint(logLimit, limit) }) @@ -89,10 +89,10 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c invocation(CommandEmailGet, get, "1"), ) if err != nil { - return Emails{}, "", err + return Emails{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Emails, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, Error) { var queryResponse EmailQueryResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) if err != nil { @@ -116,7 +116,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c } // Get all the Emails that have been created, updated or deleted since a given state. -func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) { +func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Language, Error) { logger = j.loggerParams("GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState) }) @@ -152,10 +152,10 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context. invocation(CommandEmailGet, getUpdated, "2"), ) if err != nil { - return MailboxChanges{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload) + return MailboxChanges{}, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxChanges, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, Error) { var changesResponse EmailChangesResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailChanges, "0", &changesResponse) if err != nil { @@ -195,7 +195,7 @@ type EmailSnippetQueryResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, offset uint, limit uint) (EmailSnippetQueryResult, SessionState, Error) { +func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint) (EmailSnippetQueryResult, SessionState, Language, Error) { logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { return z.Uint(logLimit, limit).Uint(logOffset, offset) }) @@ -229,10 +229,10 @@ func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, invocation(CommandSearchSnippetGet, snippet, "1"), ) if err != nil { - return EmailSnippetQueryResult{}, "", err + return EmailSnippetQueryResult{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailSnippetQueryResult, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailSnippetQueryResult, Error) { var queryResponse EmailQueryResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) if err != nil { @@ -264,7 +264,7 @@ type EmailQueryResult struct { QueryState State `json:"queryState"` } -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) { +func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryResult, SessionState, Language, Error) { logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) @@ -299,10 +299,10 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio invocation(CommandEmailGet, mails, "1"), ) if err != nil { - return EmailQueryResult{}, "", err + return EmailQueryResult{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailQueryResult, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailQueryResult, Error) { var queryResponse EmailQueryResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) if err != nil { @@ -339,7 +339,7 @@ type EmailQueryWithSnippetsResult struct { QueryState State `json:"queryState"` } -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) { +func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryWithSnippetsResult, SessionState, Language, Error) { logger = j.loggerParams("QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) @@ -387,10 +387,10 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle if err != nil { logger.Error().Err(err).Send() - 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) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailQueryWithSnippetsResult, Error) { var queryResponse EmailQueryResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) if err != nil { @@ -448,7 +448,7 @@ type UploadedEmail struct { Sha512 string `json:"sha:512"` } -func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (UploadedEmail, SessionState, Error) { +func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte) (UploadedEmail, SessionState, Language, Error) { encoded := base64.StdEncoding.EncodeToString(data) upload := BlobUploadCommand{ @@ -478,10 +478,10 @@ 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) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UploadedEmail, Error) { var uploadResponse BlobUploadResponse err = retrieveResponseMatchParameters(logger, body, CommandBlobUpload, "0", &uploadResponse) if err != nil { @@ -526,7 +526,7 @@ type CreatedEmail struct { State State `json:"state"` } -func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger) (CreatedEmail, SessionState, Error) { +func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (CreatedEmail, SessionState, Language, Error) { cmd, err := j.request(session, logger, invocation(CommandEmailSubmissionSet, EmailSetCommand{ AccountId: accountId, @@ -536,10 +536,10 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Sessi }, "0"), ) if err != nil { - return CreatedEmail{}, "", err + return CreatedEmail{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (CreatedEmail, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CreatedEmail, Error) { var setResponse EmailSetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse) if err != nil { @@ -584,7 +584,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, SessionState, Error) { +func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (UpdatedEmails, SessionState, Language, Error) { cmd, err := j.request(session, logger, invocation(CommandEmailSet, EmailSetCommand{ AccountId: accountId, @@ -592,10 +592,10 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, }, "0"), ) if err != nil { - return UpdatedEmails{}, "", err + return UpdatedEmails{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (UpdatedEmails, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UpdatedEmails, Error) { var setResponse EmailSetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse) if err != nil { @@ -616,7 +616,7 @@ type DeletedEmails struct { State State `json:"state"` } -func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger) (DeletedEmails, SessionState, Error) { +func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (DeletedEmails, SessionState, Language, Error) { cmd, err := j.request(session, logger, invocation(CommandEmailSet, EmailSetCommand{ AccountId: accountId, @@ -624,10 +624,10 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi }, "0"), ) if err != nil { - return DeletedEmails{}, "", err + return DeletedEmails{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (DeletedEmails, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (DeletedEmails, Error) { var setResponse EmailSetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse) if err != nil { @@ -666,7 +666,7 @@ type SubmittedEmail struct { MdnBlobIds []string `json:"mdnBlobIds,omitempty"` } -func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, data []byte) (SubmittedEmail, SessionState, Error) { +func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte) (SubmittedEmail, SessionState, Language, Error) { set := EmailSubmissionSetCommand{ AccountId: accountId, Create: map[string]EmailSubmissionCreate{ @@ -696,10 +696,10 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string invocation(CommandEmailSubmissionGet, get, "1"), ) if err != nil { - return SubmittedEmail{}, "", err + return SubmittedEmail{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (SubmittedEmail, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (SubmittedEmail, Error) { var submissionResponse EmailSubmissionSetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionSet, "0", &submissionResponse) if err != nil { @@ -748,7 +748,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string }) } -func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, Error) { +func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, Language, Error) { logger = j.loggerParams("EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId)) }) @@ -770,10 +770,10 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess }, "1"), ) if err != nil { - return []Email{}, "", err + return []Email{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) ([]Email, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Email, Error) { var emailsResponse EmailGetResponse err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &emailsResponse) if err != nil { @@ -789,7 +789,7 @@ type EmailsSummary struct { State State `json:"state"` } -func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, filter EmailFilterElement, limit uint) (map[string]EmailsSummary, SessionState, Error) { +func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint) (map[string]EmailsSummary, SessionState, Language, Error) { logger = j.logger("QueryEmailSummaries", session, logger) uniqueAccountIds := structs.Uniq(accountIds) @@ -815,10 +815,10 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx } cmd, err := j.request(session, logger, invocations...) if err != nil { - return map[string]EmailsSummary{}, "", err + return map[string]EmailsSummary{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]EmailsSummary, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailsSummary, Error) { resp := map[string]EmailsSummary{} for _, accountId := range uniqueAccountIds { var response EmailGetResponse diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go index 1c613efa9..0c095b4dc 100644 --- a/pkg/jmap/jmap_api_identity.go +++ b/pkg/jmap/jmap_api_identity.go @@ -14,13 +14,13 @@ type Identities struct { } // https://jmap.io/spec-mail.html#identityget -func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger) (Identities, SessionState, Error) { +func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (Identities, SessionState, Language, Error) { logger = j.logger("GetIdentity", session, logger) cmd, err := j.request(session, logger, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, "0")) if err != nil { - return Identities{}, "", err + return Identities{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Identities, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identities, Error) { var response IdentityGetResponse err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response) if err != nil { @@ -39,7 +39,7 @@ type IdentitiesGetResponse struct { State State `json:"state"` } -func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesGetResponse, SessionState, Error) { +func (j *Client) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, Language, Error) { uniqueAccountIds := structs.Uniq(accountIds) logger = j.logger("GetIdentities", session, logger) @@ -51,9 +51,9 @@ func (j *Client) GetIdentities(accountIds []string, session *Session, ctx contex cmd, err := j.request(session, logger, calls...) if err != nil { - return IdentitiesGetResponse{}, "", err + return IdentitiesGetResponse{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentitiesGetResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentitiesGetResponse, Error) { identities := make(map[string][]Identity, len(uniqueAccountIds)) var lastState State notFound := []string{} @@ -84,7 +84,7 @@ type IdentitiesAndMailboxesGetResponse struct { Mailboxes []Mailbox `json:"mailboxes"` } -func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (IdentitiesAndMailboxesGetResponse, SessionState, Error) { +func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesAndMailboxesGetResponse, SessionState, Language, Error) { uniqueAccountIds := structs.Uniq(accountIds) logger = j.logger("GetIdentitiesAndMailboxes", session, logger) @@ -97,9 +97,9 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [ cmd, err := j.request(session, logger, calls...) if err != nil { - return IdentitiesAndMailboxesGetResponse{}, "", err + return IdentitiesAndMailboxesGetResponse{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentitiesAndMailboxesGetResponse, Error) { identities := make(map[string][]Identity, len(uniqueAccountIds)) var lastState State notFound := []string{} diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go index 558b67428..ed0ba62af 100644 --- a/pkg/jmap/jmap_api_mailbox.go +++ b/pkg/jmap/jmap_api_mailbox.go @@ -16,17 +16,17 @@ type MailboxesResponse struct { } // https://jmap.io/spec-mail.html#mailboxget -func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string) (MailboxesResponse, SessionState, Error) { +func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, Language, Error) { logger = j.logger("GetMailbox", session, logger) cmd, err := j.request(session, logger, invocation(CommandMailboxGet, MailboxGetCommand{AccountId: accountId, Ids: ids}, "0"), ) if err != nil { - return MailboxesResponse{}, "", err + return MailboxesResponse{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxesResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxesResponse, Error) { var response MailboxGetResponse err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "0", &response) if err != nil { @@ -45,13 +45,13 @@ type AllMailboxesResponse struct { State State `json:"state"` } -func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (map[string]AllMailboxesResponse, SessionState, Error) { +func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AllMailboxesResponse, SessionState, Language, Error) { logger = j.logger("GetAllMailboxes", session, logger) uniqueAccountIds := structs.Uniq(accountIds) n := len(uniqueAccountIds) if n < 1 { - return map[string]AllMailboxesResponse{}, "", nil + return map[string]AllMailboxesResponse{}, "", "", nil } invocations := make([]Invocation, n) @@ -61,10 +61,10 @@ func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx cont cmd, err := j.request(session, logger, invocations...) if err != nil { - return map[string]AllMailboxesResponse{}, "", err + return map[string]AllMailboxesResponse{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]AllMailboxesResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]AllMailboxesResponse, Error) { resp := map[string]AllMailboxesResponse{} for _, accountId := range uniqueAccountIds { var response MailboxGetResponse @@ -89,7 +89,7 @@ type Mailboxes struct { State State `json:"state,omitempty"` } -func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, filter MailboxFilterElement) (map[string]Mailboxes, SessionState, Error) { +func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter MailboxFilterElement) (map[string]Mailboxes, SessionState, Language, Error) { logger = j.logger("SearchMailboxes", session, logger) uniqueAccountIds := structs.Uniq(accountIds) @@ -108,10 +108,10 @@ func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx cont } cmd, err := j.request(session, logger, invocations...) if err != nil { - return map[string]Mailboxes{}, "", err + return map[string]Mailboxes{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]Mailboxes, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]Mailboxes, Error) { resp := map[string]Mailboxes{} for _, accountId := range uniqueAccountIds { var response MailboxGetResponse @@ -136,7 +136,7 @@ type MailboxChanges struct { } // Retrieve Email changes in a given Mailbox since a given state. -func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) { +func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Language, Error) { logger = j.loggerParams("GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState) }) @@ -172,10 +172,10 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte invocation(CommandEmailGet, getUpdated, "2"), ) if err != nil { - return MailboxChanges{}, "", err + return MailboxChanges{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxChanges, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, Error) { var mailboxResponse MailboxChangesResponse err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, "0", &mailboxResponse) if err != nil { @@ -208,7 +208,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte } // Retrieve Email changes in Mailboxes of multiple Accounts. -func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, sinceStateMap map[string]string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (map[string]MailboxChanges, SessionState, Error) { +func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (map[string]MailboxChanges, SessionState, Language, Error) { logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context { sinceStateLogDict := zerolog.Dict() for k, v := range sinceStateMap { @@ -220,7 +220,7 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi uniqueAccountIds := structs.Uniq(accountIds) n := len(uniqueAccountIds) if n < 1 { - return map[string]MailboxChanges{}, "", nil + return map[string]MailboxChanges{}, "", "", nil } invocations := make([]Invocation, n*3) @@ -262,10 +262,10 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi cmd, err := j.request(session, logger, invocations...) if err != nil { - return map[string]MailboxChanges{}, "", err + return map[string]MailboxChanges{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]MailboxChanges, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]MailboxChanges, Error) { resp := make(map[string]MailboxChanges, n) for _, accountId := range uniqueAccountIds { var mailboxResponse MailboxChangesResponse @@ -300,13 +300,13 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi }) } -func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger) (map[string][]string, SessionState, Error) { +func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]string, SessionState, Language, Error) { logger = j.logger("GetMailboxRolesForMultipleAccounts", session, logger) uniqueAccountIds := structs.Uniq(accountIds) n := len(uniqueAccountIds) if n < 1 { - return map[string][]string{}, "", nil + return map[string][]string{}, "", "", nil } t := true @@ -331,10 +331,10 @@ func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session cmd, err := j.request(session, logger, invocations...) if err != nil { - return map[string][]string{}, "", err + return map[string][]string{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string][]string, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]string, Error) { resp := make(map[string][]string, n) for _, accountId := range uniqueAccountIds { var getResponse MailboxGetResponse diff --git a/pkg/jmap/jmap_api_quota.go b/pkg/jmap/jmap_api_quota.go new file mode 100644 index 000000000..a8082ca80 --- /dev/null +++ b/pkg/jmap/jmap_api_quota.go @@ -0,0 +1,23 @@ +package jmap + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" +) + +func (j *Client) GetQuotas(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (QuotaGetResponse, SessionState, Language, Error) { + logger = j.logger("GetQuotas", session, logger) + cmd, err := j.request(session, logger, invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId}, "0")) + if err != nil { + return QuotaGetResponse{}, "", "", err + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (QuotaGetResponse, Error) { + var response QuotaGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandQuotaGet, "0", &response) + if err != nil { + return QuotaGetResponse{}, err + } + return response, nil + }) +} diff --git a/pkg/jmap/jmap_api_vacation.go b/pkg/jmap/jmap_api_vacation.go index ca8824d7a..c2221df7a 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, SessionState, Error) { +func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseGetResponse, SessionState, Language, Error) { logger = j.logger("GetVacationResponse", session, logger) cmd, err := j.request(session, logger, invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "0")) if err != nil { - return VacationResponseGetResponse{}, "", err + return VacationResponseGetResponse{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseGetResponse, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponseGetResponse, Error) { var response VacationResponseGetResponse err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseGet, "0", &response) if err != nil { @@ -58,7 +58,7 @@ type VacationResponseChange struct { ResponseState State `json:"state"` } -func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, SessionState, Error) { +func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseChange, SessionState, Language, Error) { logger = j.logger("SetVacationResponse", session, logger) cmd, err := j.request(session, logger, @@ -80,9 +80,9 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "1"), ) if err != nil { - return VacationResponseChange{}, "", err + return VacationResponseChange{}, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseChange, Error) { + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponseChange, Error) { var setResponse VacationResponseSetResponse err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseSet, "0", &setResponse) if err != nil { diff --git a/pkg/jmap/jmap_http.go b/pkg/jmap/jmap_http.go index 62404d556..ff62eaedb 100644 --- a/pkg/jmap/jmap_http.go +++ b/pkg/jmap/jmap_http.go @@ -176,7 +176,7 @@ func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger return data, nil } -func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) { +func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) { jmapUrl := session.JmapUrl.String() endpoint := session.JmapEndpoint logger = log.From(logger.With().Str(logEndpoint, endpoint)) @@ -184,14 +184,21 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio bodyBytes, err := json.Marshal(request) if err != nil { logger.Error().Err(err).Msg("failed to marshall JSON payload") - return nil, SimpleError{code: JmapErrorEncodingRequestBody, err: err} + return nil, "", SimpleError{code: JmapErrorEncodingRequestBody, err: err} } req, err := http.NewRequestWithContext(ctx, http.MethodPost, jmapUrl, bytes.NewBuffer(bodyBytes)) if err != nil { logger.Error().Err(err).Msgf("failed to create POST request for %v", jmapUrl) - return nil, SimpleError{code: JmapErrorCreatingRequest, err: err} + return nil, "", SimpleError{code: JmapErrorCreatingRequest, err: err} } + + // Some JMAP APIs use the Accept-Language header to determine which language to use to translate + // texts in attributes. + if acceptLanguage != "" { + req.Header.Add("Accept-Language", acceptLanguage) + } + req.Header.Add("Content-Type", "application/json") req.Header.Add("User-Agent", h.userAgent) h.auth(session.Username, logger, req) @@ -200,12 +207,13 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio if err != nil { h.listener.OnFailedRequest(endpoint, err) logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl) - return nil, SimpleError{code: JmapErrorSendingRequest, err: err} + return nil, "", SimpleError{code: JmapErrorSendingRequest, err: err} } + language := Language(res.Header.Get("Content-Language")) if res.StatusCode < 200 || res.StatusCode > 299 { h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode) logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Msg("HTTP response status code is not 2xx") - return nil, SimpleError{code: JmapErrorServerResponse, err: err} + return nil, language, SimpleError{code: JmapErrorServerResponse, err: err} } if res.Body != nil { defer func(Body io.ReadCloser) { @@ -221,34 +229,38 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio if err != nil { logger.Error().Err(err).Msg("failed to read response body") h.listener.OnResponseBodyReadingError(endpoint, err) - return nil, SimpleError{code: JmapErrorServerResponse, err: err} + return nil, language, SimpleError{code: JmapErrorServerResponse, err: err} } - return body, nil + return body, language, nil } -func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, body io.Reader) (UploadedBlob, Error) { +func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, body io.Reader) (UploadedBlob, Language, Error) { logger = log.From(logger.With().Str(logEndpoint, endpoint)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, body) if err != nil { logger.Error().Err(err).Msgf("failed to create POST request for %v", uploadUrl) - return UploadedBlob{}, SimpleError{code: JmapErrorCreatingRequest, err: err} + return UploadedBlob{}, "", SimpleError{code: JmapErrorCreatingRequest, err: err} } req.Header.Add("Content-Type", contentType) req.Header.Add("User-Agent", h.userAgent) h.auth(session.Username, logger, req) + if acceptLanguage != "" { + req.Header.Add("Accept-Language", acceptLanguage) + } res, err := h.client.Do(req) if err != nil { h.listener.OnFailedRequest(endpoint, err) logger.Error().Err(err).Msgf("failed to perform POST %v", uploadUrl) - return UploadedBlob{}, SimpleError{code: JmapErrorSendingRequest, err: err} + return UploadedBlob{}, "", SimpleError{code: JmapErrorSendingRequest, err: err} } + language := Language(res.Header.Get("Content-Language")) if res.StatusCode < 200 || res.StatusCode > 299 { h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode) logger.Error().Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx") - return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err} + return UploadedBlob{}, language, SimpleError{code: JmapErrorServerResponse, err: err} } if res.Body != nil { defer func(Body io.ReadCloser) { @@ -264,7 +276,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s if err != nil { logger.Error().Err(err).Msg("failed to read response body") h.listener.OnResponseBodyReadingError(endpoint, err) - return UploadedBlob{}, SimpleError{code: JmapErrorServerResponse, err: err} + return UploadedBlob{}, language, SimpleError{code: JmapErrorServerResponse, err: err} } var result UploadedBlob @@ -272,36 +284,40 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s if err != nil { logger.Error().Str(logHttpUrl, uploadUrl).Err(err).Msg("failed to decode JSON payload from the upload response") h.listener.OnResponseBodyUnmarshallingError(endpoint, err) - return UploadedBlob{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err} + return UploadedBlob{}, language, SimpleError{code: JmapErrorDecodingResponseBody, err: err} } - return result, nil + return result, language, nil } -func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error) { +func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) { logger = log.From(logger.With().Str(logEndpoint, endpoint)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) if err != nil { logger.Error().Err(err).Msgf("failed to create GET request for %v", downloadUrl) - return nil, SimpleError{code: JmapErrorCreatingRequest, err: err} + return nil, "", SimpleError{code: JmapErrorCreatingRequest, err: err} } req.Header.Add("User-Agent", h.userAgent) h.auth(session.Username, logger, req) + if acceptLanguage != "" { + req.Header.Add("Accept-Language", acceptLanguage) + } res, err := h.client.Do(req) if err != nil { h.listener.OnFailedRequest(endpoint, err) logger.Error().Err(err).Msgf("failed to perform GET %v", downloadUrl) - return nil, SimpleError{code: JmapErrorSendingRequest, err: err} + return nil, "", SimpleError{code: JmapErrorSendingRequest, err: err} } + language := Language(res.Header.Get("Content-Language")) if res.StatusCode == http.StatusNotFound { - return nil, nil + return nil, language, nil } if res.StatusCode < 200 || res.StatusCode > 299 { h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode) logger.Error().Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx") - return nil, SimpleError{code: JmapErrorServerResponse, err: err} + return nil, language, SimpleError{code: JmapErrorServerResponse, err: err} } h.listener.OnSuccessfulRequest(endpoint, res.StatusCode) @@ -321,7 +337,7 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, Type: res.Header.Get("Content-Type"), ContentDisposition: res.Header.Get("Content-Disposition"), CacheControl: res.Header.Get("Cache-Control"), - }, nil + }, language, nil } type WebSocketPushEnable struct { diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go index b46ff851f..ba943a170 100644 --- a/pkg/jmap/jmap_integration_test.go +++ b/pkg/jmap/jmap_integration_test.go @@ -363,7 +363,7 @@ func TestWithStalwart(t *testing.T) { var inboxFolder string var inboxId string { - respByAccountId, sessionState, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger) + respByAccountId, sessionState, _, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger, "") require.NoError(err) require.Equal(session.State, sessionState) require.Len(respByAccountId, 1) @@ -440,7 +440,7 @@ func TestWithStalwart(t *testing.T) { { { - resp, sessionState, err := j.GetIdentity(accountId, session, ctx, logger) + resp, sessionState, _, err := j.GetIdentity(accountId, session, ctx, logger, "") require.NoError(err) require.Equal(session.State, sessionState) require.Len(resp.Identities, 1) @@ -449,7 +449,7 @@ func TestWithStalwart(t *testing.T) { } { - respByAccountId, sessionState, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger) + respByAccountId, sessionState, _, err := j.GetAllMailboxes([]string{accountId}, session, ctx, logger, "") require.NoError(err) require.Equal(session.State, sessionState) require.Len(respByAccountId, 1) @@ -465,7 +465,7 @@ func TestWithStalwart(t *testing.T) { } { - resp, sessionState, err := j.GetAllEmailsInMailbox(accountId, session, ctx, logger, inboxId, 0, 0, false, 0) + resp, sessionState, _, err := j.GetAllEmailsInMailbox(accountId, session, ctx, logger, "", inboxId, 0, 0, false, 0) require.NoError(err) require.Equal(session.State, sessionState) diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index e068f124a..60aec96bb 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -865,6 +865,8 @@ type SessionState string type State string +type Language string + type SessionResponse struct { Capabilities SessionCapabilities `json:"capabilities"` @@ -4558,6 +4560,18 @@ type SendMDN struct { OnSuccessUpdateEmail map[string]PatchObject `json:"onSuccessUpdateEmail,omitempty"` } +type QuotaGetCommand struct { + AccountId string `json:"accountId"` + Ids []string `json:"ids,omitempty"` +} + +type QuotaGetResponse struct { + AccountId string `json:"accountId"` + State State `json:"state,omitempty"` + List []Quota `json:"list,omitempty"` + NotFound []string `json:"notFound,omitempty"` +} + type ErrorResponse struct { Type string `json:"type"` Description string `json:"description,omitempty"` @@ -4582,6 +4596,7 @@ const ( CommandVacationResponseGet Command = "VacationResponse/get" CommandVacationResponseSet Command = "VacationResponse/set" CommandSearchSnippetGet Command = "SearchSnippet/get" + CommandQuotaGet Command = "Quota/get" ) var CommandResponseTypeMap = map[Command]func() any{ @@ -4601,4 +4616,5 @@ var CommandResponseTypeMap = map[Command]func() any{ CommandVacationResponseGet: func() any { return VacationResponseGetResponse{} }, CommandVacationResponseSet: func() any { return VacationResponseSetResponse{} }, CommandSearchSnippetGet: func() any { return SearchSnippetGetResponse{} }, + CommandQuotaGet: func() any { return QuotaGetResponse{} }, } diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go index cd3fcdd2e..f77339c82 100644 --- a/pkg/jmap/jmap_test.go +++ b/pkg/jmap/jmap_test.go @@ -117,10 +117,10 @@ func NewTestJmapBlobClient(t *testing.T) BlobClient { return &TestJmapBlobClient{t: t} } -func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, body io.Reader) (UploadedBlob, Error) { +func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, body io.Reader) (UploadedBlob, Language, Error) { bytes, err := io.ReadAll(body) if err != nil { - return UploadedBlob{}, SimpleError{code: 0, err: err} + return UploadedBlob{}, "", SimpleError{code: 0, err: err} } hasher := sha512.New() hasher.Write(bytes) @@ -129,17 +129,17 @@ func (t TestJmapBlobClient) UploadBinary(ctx context.Context, logger *log.Logger Size: len(bytes), Type: contentType, Sha512: base64.StdEncoding.EncodeToString(hasher.Sum(nil)), - }, nil + }, "", nil } -func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error) { +func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) { return &BlobDownload{ Body: io.NopCloser(strings.NewReader("")), Size: -1, Type: "text/plain", ContentDisposition: "attachment; filename=\"file.txt\"", CacheControl: "", - }, nil + }, "", nil } func (t TestJmapBlobClient) Close() error { @@ -164,24 +164,24 @@ func (t TestWsClientFactory) Close() error { return nil } -func serveTestFile(t *testing.T, name string) ([]byte, Error) { +func serveTestFile(t *testing.T, name string) ([]byte, Language, Error) { cwd, _ := os.Getwd() p := filepath.Join(cwd, "testdata", name) bytes, err := os.ReadFile(p) if err != nil { - return bytes, SimpleError{code: 0, err: err} + return bytes, "", SimpleError{code: 0, err: err} } // try to parse it first to avoid any deeper issues that are caused by the test tools var target map[string]any err = json.Unmarshal(bytes, &target) if err != nil { t.Errorf("failed to parse JSON test data file '%v': %v", p, err) - return nil, SimpleError{code: 0, err: err} + return nil, "", SimpleError{code: 0, err: err} } - return bytes, nil + return bytes, "", nil } -func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request) ([]byte, Error) { +func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) { command := request.MethodCalls[0].Command switch command { case CommandMailboxGet: @@ -190,7 +190,7 @@ func (t *TestJmapApiClient) Command(ctx context.Context, logger *log.Logger, ses return serveTestFile(t.t, "mails1.json") default: require.Fail(t.t, "TestJmapApiClient: unsupported jmap command: %v", command) - return nil, SimpleError{code: 0, err: fmt.Errorf("TestJmapApiClient: unsupported jmap command: %v", command)} + return nil, "", SimpleError{code: 0, err: fmt.Errorf("TestJmapApiClient: unsupported jmap command: %v", command)} } } @@ -231,7 +231,7 @@ func TestRequests(t *testing.T) { }, } - foldersByAccountId, sessionState, err := client.GetAllMailboxes([]string{"a"}, &session, ctx, &logger) + foldersByAccountId, sessionState, _, err := client.GetAllMailboxes([]string{"a"}, &session, ctx, &logger, "") require.NoError(err) require.Len(foldersByAccountId, 1) require.Contains(foldersByAccountId, "a") @@ -239,7 +239,7 @@ func TestRequests(t *testing.T) { require.Len(folders.Mailboxes, 5) require.NotEmpty(sessionState) - emails, sessionState, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0) + emails, sessionState, _, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "", "Inbox", 0, 0, true, 0) require.NoError(err) require.Len(emails.Emails, 3) require.NotEmpty(sessionState) diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go index 4db5c4709..6f2337eab 100644 --- a/pkg/jmap/jmap_tools.go +++ b/pkg/jmap/jmap_tools.go @@ -57,12 +57,13 @@ func command[T any](api ApiClient, session *Session, sessionOutdatedHandler func(session *Session, newState SessionState), request Request, - mapper func(body *Response) (T, Error)) (T, SessionState, Error) { + acceptLanguage string, + mapper func(body *Response) (T, Error)) (T, SessionState, Language, Error) { - responseBody, jmapErr := api.Command(ctx, logger, session, request) + responseBody, language, jmapErr := api.Command(ctx, logger, session, request, acceptLanguage) if jmapErr != nil { var zero T - return zero, "", jmapErr + return zero, "", language, jmapErr } var response Response @@ -70,7 +71,7 @@ func command[T any](api ApiClient, if err != nil { logger.Error().Err(err).Msgf("failed to deserialize body JSON payload into a %T", response) var zero T - return zero, "", SimpleError{code: JmapErrorDecodingResponseBody, err: err} + return zero, "", language, SimpleError{code: JmapErrorDecodingResponseBody, err: err} } if response.SessionState != session.State { @@ -117,21 +118,21 @@ func command[T any](api ApiClient, err = errors.New(msg) logger.Warn().Int("code", code).Str("type", errorParameters.Type).Msg(msg) var zero T - return zero, response.SessionState, SimpleError{code: code, err: err} + return zero, response.SessionState, language, SimpleError{code: code, err: err} } else { code := JmapErrorUnspecifiedType msg := fmt.Sprintf("found method level error in response '%v'", mr.Tag) err := errors.New(msg) logger.Warn().Int("code", code).Msg(msg) var zero T - return zero, response.SessionState, SimpleError{code: code, err: err} + return zero, response.SessionState, language, SimpleError{code: code, err: err} } } } result, jerr := mapper(&response) sessionState := response.SessionState - return result, sessionState, jerr + return result, sessionState, language, jerr } func mapstructStringToTimeHook() mapstructure.DecodeHookFunc { diff --git a/pkg/jmap/jmap_tools_test.go b/pkg/jmap/jmap_tools_test.go index 2c0beea6d..16a1b0b01 100644 --- a/pkg/jmap/jmap_tools_test.go +++ b/pkg/jmap/jmap_tools_test.go @@ -9,7 +9,7 @@ import ( func TestDeserializeMailboxGetResponse(t *testing.T) { require := require.New(t) - jsonBytes, jmapErr := serveTestFile(t, "mailboxes1.json") + jsonBytes, _, jmapErr := serveTestFile(t, "mailboxes1.json") require.NoError(jmapErr) var data Response err := json.Unmarshal(jsonBytes, &data) @@ -66,7 +66,7 @@ func TestDeserializeMailboxGetResponse(t *testing.T) { func TestDeserializeEmailGetResponse(t *testing.T) { require := require.New(t) - jsonBytes, jmapErr := serveTestFile(t, "mails1.json") + jsonBytes, _, jmapErr := serveTestFile(t, "mails1.json") require.NoError(jmapErr) var data Response err := json.Unmarshal(jsonBytes, &data) diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml index d0fd05764..0becebd89 100644 --- a/services/groupware/apidoc.yml +++ b/services/groupware/apidoc.yml @@ -35,6 +35,9 @@ tags: - name: task x-displayName: Tasks description: APIs about tasks + - name: quota + x-displayName: Quota + description: APIs about quotas - name: vacation x-displayName: Vacation Responses description: APIs about vacation responses @@ -63,6 +66,9 @@ x-tagGroups: tags: - tasklist - task + - name: Quotas + tags: + - quota components: securitySchemes: api: diff --git a/services/groupware/pkg/groupware/groupware_api_account.go b/services/groupware/pkg/groupware/groupware_api_account.go index 10500fb34..613c4a389 100644 --- a/services/groupware/pkg/groupware/groupware_api_account.go +++ b/services/groupware/pkg/groupware/groupware_api_account.go @@ -32,7 +32,7 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) { if err != nil { return errorResponse(err) } - return response(account, req.session.State) + return response(account, req.session.State, "") }) } @@ -54,7 +54,7 @@ type SwaggerGetAccountsResponse struct { // 500: ErrorResponse500 func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - return response(req.session.Accounts, req.session.State) + return response(req.session.Accounts, req.session.State, "") }) } @@ -107,7 +107,7 @@ func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request) logger := log.From(req.logger.With().Str(logAccountId, mailAccountId)) accountIds := structs.Keys(req.session.Accounts) - resp, sessionState, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, logger) + resp, sessionState, lang, jerr := g.jmap.GetIdentitiesAndMailboxes(mailAccountId, accountIds, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -121,6 +121,6 @@ func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request) Mailboxes: map[string][]jmap.Mailbox{ mailAccountId: resp.Mailboxes, }, - }, sessionState) + }, sessionState, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_blob.go b/services/groupware/pkg/groupware/groupware_api_blob.go index 6187a585f..e47930365 100644 --- a/services/groupware/pkg/groupware/groupware_api_blob.go +++ b/services/groupware/pkg/groupware/groupware_api_blob.go @@ -28,7 +28,7 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) { } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - res, sessionState, jerr := g.jmap.GetBlobMetadata(accountId, req.session, req.ctx, logger, blobId) + res, sessionState, lang, jerr := g.jmap.GetBlobMetadata(accountId, req.session, req.ctx, logger, req.language(), blobId) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -38,9 +38,9 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) { } digest := blob.Digest() if digest != "" { - return etagResponse(res, sessionState, jmap.State(digest)) + return etagResponse(res, sessionState, jmap.State(digest), lang) } else { - return response(res, sessionState) + return response(res, sessionState, lang) } }) } @@ -64,12 +64,12 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) { } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - resp, jerr := g.jmap.UploadBlobStream(accountId, req.session, req.ctx, logger, contentType, body) + resp, lang, jerr := g.jmap.UploadBlobStream(accountId, req.session, req.ctx, logger, contentType, req.language(), body) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagOnlyResponse(resp, jmap.State(resp.Sha512)) + return etagOnlyResponse(resp, jmap.State(resp.Sha512), lang) }) } @@ -89,7 +89,7 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) { } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - blob, jerr := g.jmap.DownloadBlobStream(accountId, blobId, name, typ, req.session, req.ctx, logger) + blob, lang, jerr := g.jmap.DownloadBlobStream(accountId, blobId, name, typ, req.session, req.ctx, logger, req.language()) if blob != nil && blob.Body != nil { defer func(Body io.ReadCloser) { err := Body.Close() @@ -118,6 +118,9 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) { if blob.Size >= 0 { w.Header().Add("Content-Size", strconv.Itoa(blob.Size)) } + if lang != "" { + w.Header().Add("Content-Language", string(lang)) + } _, err := io.Copy(w, blob.Body) if err != nil { diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go index 732b78cd5..e7f4ba7ba 100644 --- a/services/groupware/pkg/groupware/groupware_api_calendars.go +++ b/services/groupware/pkg/groupware/groupware_api_calendars.go @@ -31,7 +31,7 @@ func (g *Groupware) GetCalendars(w http.ResponseWriter, r *http.Request) { } var _ string = accountId - return response(AllCalendars, req.session.State) + return response(AllCalendars, req.session.State, "") }) } @@ -65,7 +65,7 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { // TODO replace with proper implementation for _, calendar := range AllCalendars { if calendar.Id == calendarId { - return response(calendar, req.session.State) + return response(calendar, req.session.State, "") } } return notFoundResponse(req.session.State) @@ -102,6 +102,6 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) if !ok { return notFoundResponse(req.session.State) } - return response(events, req.session.State) + return response(events, req.session.State, "") }) } diff --git a/services/groupware/pkg/groupware/groupware_api_contacts.go b/services/groupware/pkg/groupware/groupware_api_contacts.go index 75579d3e7..433600269 100644 --- a/services/groupware/pkg/groupware/groupware_api_contacts.go +++ b/services/groupware/pkg/groupware/groupware_api_contacts.go @@ -33,7 +33,7 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) { var _ string = accountId // TODO replace with proper implementation - return response(AllAddressBooks, req.session.State) + return response(AllAddressBooks, req.session.State, "") }) } @@ -67,7 +67,7 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) { // TODO replace with proper implementation for _, ab := range AllAddressBooks { if ab.Id == addressBookId { - return response(ab, req.session.State) + return response(ab, req.session.State, "") } } return notFoundResponse(req.session.State) @@ -104,6 +104,6 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ if !ok { return notFoundResponse(req.session.State) } - return response(contactCards, req.session.State) + return response(contactCards, req.session.State, "") }) } diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go index f676e717e..b08e337cf 100644 --- a/services/groupware/pkg/groupware/groupware_api_emails.go +++ b/services/groupware/pkg/groupware/groupware_api_emails.go @@ -77,12 +77,12 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request logger := log.From(req.logger.With().Str(HeaderSince, since).Str(logAccountId, accountId)) - emails, sessionState, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges) + emails, sessionState, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.maxBodyValueBytes, maxChanges) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(emails, sessionState, emails.State) + return etagResponse(emails, sessionState, emails.State, lang) }) } else { g.respond(w, r, func(req Request) Response { @@ -114,12 +114,12 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request logger := log.From(l) - emails, sessionState, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes) + emails, sessionState, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, true, g.maxBodyValueBytes) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(emails, sessionState, emails.State) + return etagResponse(emails, sessionState, emails.State, lang) }) } } @@ -140,25 +140,25 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) if len(ids) == 1 { logger := log.From(l.Str("id", log.SafeString(id))) - emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes) if jerr != nil { return req.errorResponseFromJmap(jerr) } if len(emails.Emails) < 1 { return notFoundResponse(sessionState) } else { - return etagResponse(emails.Emails[0], sessionState, emails.State) + return etagResponse(emails.Emails[0], sessionState, emails.State, lang) } } else { logger := log.From(l.Array("ids", log.SafeStringArray(ids))) - emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, ids, true, g.maxBodyValueBytes) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes) if jerr != nil { return req.errorResponseFromJmap(jerr) } if len(emails.Emails) < 1 { return notFoundResponse(sessionState) } else { - return etagResponse(emails.Emails, sessionState, emails.State) + return etagResponse(emails.Emails, sessionState, emails.State, lang) } } }) @@ -196,7 +196,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) } l := req.logger.With().Str(logAccountId, accountId) logger := log.From(l) - emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, []string{id}, false, 0) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -204,7 +204,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) return notFoundResponse(sessionState) } email := emails.Emails[0] - return etagResponse(email.Attachments, sessionState, emails.State) + return etagResponse(email.Attachments, sessionState, emails.State, lang) }) } else { g.stream(w, r, func(req Request, w http.ResponseWriter) *Error { @@ -221,7 +221,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) l = contextAppender(l) logger := log.From(l) - emails, _, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, []string{id}, false, 0) + emails, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0) if jerr != nil { return req.apiErrorFromJmap(req.observeJmapError(jerr)) } @@ -241,7 +241,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) return nil } - blob, jerr := g.jmap.DownloadBlobStream(blobAccountId, attachment.BlobId, attachment.Name, attachment.Type, req.session, req.ctx, logger) + blob, lang, jerr := g.jmap.DownloadBlobStream(blobAccountId, attachment.BlobId, attachment.Name, attachment.Type, req.session, req.ctx, logger, req.language()) if blob != nil && blob.Body != nil { defer func(Body io.ReadCloser) { err := Body.Close() @@ -270,7 +270,9 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) if blob.Size >= 0 { w.Header().Add("Content-Size", strconv.Itoa(blob.Size)) } - + if lang != "" { + w.Header().Add("Content-Language", string(lang)) + } _, err := io.Copy(w, blob.Body) if err != nil { return req.observedParameterError(ErrorStreamingResponse) @@ -300,12 +302,12 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since logger := log.From(l) - emails, sessionState, jerr := g.jmap.GetEmailsSince(accountId, req.session, req.ctx, logger, since, true, g.maxBodyValueBytes, maxChanges) + emails, sessionState, lang, jerr := g.jmap.GetEmailsSince(accountId, req.session, req.ctx, logger, req.language(), since, true, g.maxBodyValueBytes, maxChanges) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(emails, sessionState, emails.State) + return etagResponse(emails, sessionState, emails.State, lang) }) } @@ -497,7 +499,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { } logger = log.From(logger.With().Str(logAccountId, accountId)) - results, sessionState, jerr := g.jmap.QueryEmailsWithSnippets(accountId, filter, req.session, req.ctx, logger, offset, limit, fetchBodies, g.maxBodyValueBytes) + results, sessionState, lang, jerr := g.jmap.QueryEmailsWithSnippets(accountId, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -522,7 +524,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { Total: results.Total, Limit: results.Limit, QueryState: results.QueryState, - }, sessionState, results.QueryState) + }, sessionState, results.QueryState, lang) } else { accountId, err := req.GetAccountIdForMail() if err != nil { @@ -530,7 +532,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { } logger = log.From(logger.With().Str(logAccountId, accountId)) - results, sessionState, jerr := g.jmap.QueryEmailSnippets(accountId, filter, req.session, req.ctx, logger, offset, limit) + results, sessionState, lang, jerr := g.jmap.QueryEmailSnippets(accountId, filter, req.session, req.ctx, logger, req.language(), offset, limit) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -540,7 +542,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { Total: results.Total, Limit: results.Limit, QueryState: results.QueryState, - }, sessionState, results.QueryState) + }, sessionState, results.QueryState, lang) } }) } @@ -608,12 +610,12 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) { BodyValues: body.BodyValues, } - created, sessionState, jerr := g.jmap.CreateEmail(accountId, create, req.session, req.ctx, logger) + created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return response(created.Email, sessionState) + return response(created.Email, sessionState, lang) }) } @@ -642,7 +644,7 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { emailId: body, } - result, sessionState, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger) + result, sessionState, lang, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -657,7 +659,7 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) } - return response(updatedEmail, sessionState) + return response(updatedEmail, sessionState, lang) }) } @@ -677,7 +679,7 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) { logger := log.From(l) - _, sessionState, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger) + _, sessionState, _, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -687,14 +689,16 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) { } type AboutEmailsEvent struct { - Id string `json:"id"` - Source string `json:"source"` - Emails []jmap.Email `json:"emails"` + Id string `json:"id"` + Source string `json:"source"` + Emails []jmap.Email `json:"emails"` + Language jmap.Language `json:"lang"` } type AboutEmailResponse struct { - Email jmap.Email `json:"email"` - RequestId string `json:"requestId"` + Email jmap.Email `json:"email"` + RequestId string `json:"requestId"` + Language jmap.Language `json:"lang"` } func relatedEmails(email jmap.Email, beacon time.Time, days uint) jmap.EmailFilterElement { @@ -766,7 +770,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { reqId := req.GetRequestId() getEmailsBefore := time.Now() - emails, sessionState, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, []string{id}, true, g.maxBodyValueBytes) + emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes) getEmailsDuration := time.Since(getEmailsBefore) if jerr != nil { return req.errorResponseFromJmap(jerr) @@ -791,7 +795,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { g.job(logger, RelationTypeSameSender, func(jobId uint64, l *log.Logger) { before := time.Now() - results, _, jerr := g.jmap.QueryEmails(accountId, filter, req.session, bgctx, l, 0, limit, false, g.maxBodyValueBytes) + results, _, lang, jerr := g.jmap.QueryEmails(accountId, filter, req.session, bgctx, l, req.language(), 0, limit, false, g.maxBodyValueBytes) duration := time.Since(before) if jerr != nil { req.observeJmapError(jerr) @@ -801,14 +805,14 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { related := filterEmails(results.Emails, email) l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameSender, len(related)) if len(related) > 0 { - req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender}) + req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender, Language: lang}) } } }) g.job(logger, RelationTypeSameThread, func(jobId uint64, l *log.Logger) { before := time.Now() - emails, _, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, false, g.maxBodyValueBytes) + emails, _, _, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, req.language(), false, g.maxBodyValueBytes) duration := time.Since(before) if jerr != nil { req.observeJmapError(jerr) @@ -818,7 +822,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { related := filterEmails(emails, email) l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameThread, len(related)) if len(related) > 0 { - req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameThread}) + req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameThread, Language: lang}) } } }) @@ -826,7 +830,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { return etagResponse(AboutEmailResponse{ Email: email, RequestId: reqId, - }, sessionState, emails.State) + }, sessionState, emails.State, lang) }) } @@ -1113,7 +1117,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, logger := log.From(l) - emailsSummariesByAccount, sessionState, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, filter, limit) + emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit) if jerr != nil { return req.errorResponseFromJmap(jerr) } @@ -1141,7 +1145,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, summaries[i] = summarizeEmail(all[i].accountId, all[i].email) } - return response(summaries, sessionState) + return response(summaries, sessionState, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go index 2c77a41a2..1cdd1618b 100644 --- a/services/groupware/pkg/groupware/groupware_api_identity.go +++ b/services/groupware/pkg/groupware/groupware_api_identity.go @@ -32,10 +32,10 @@ func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) { return errorResponse(err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - res, sessionState, jerr := g.jmap.GetIdentity(accountId, req.session, req.ctx, logger) + res, sessionState, lang, jerr := g.jmap.GetIdentity(accountId, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(res, sessionState, res.State) + return etagResponse(res, sessionState, res.State, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_index.go b/services/groupware/pkg/groupware/groupware_api_index.go index 2adcb34d9..b5594e5f1 100644 --- a/services/groupware/pkg/groupware/groupware_api_index.go +++ b/services/groupware/pkg/groupware/groupware_api_index.go @@ -152,7 +152,7 @@ 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, sessionState, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger) + identitiesResponse, sessionState, lang, err := g.jmap.GetIdentities(accountIds, req.session, req.ctx, req.logger, req.language()) 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), - }, sessionState) + }, sessionState, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go index 2472e6a21..9cb5d5749 100644 --- a/services/groupware/pkg/groupware/groupware_api_mailbox.go +++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go @@ -41,13 +41,13 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) { return errorResponse(err) } - mailboxes, sessionState, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, []string{mailboxId}) + mailboxes, sessionState, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId}) if jerr != nil { return req.errorResponseFromJmap(jerr) } if len(mailboxes.Mailboxes) == 1 { - return etagResponse(mailboxes.Mailboxes[0], sessionState, mailboxes.State) + return etagResponse(mailboxes.Mailboxes[0], sessionState, mailboxes.State, lang) } else { return notFoundResponse(sessionState) } @@ -123,25 +123,25 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { logger := log.From(req.logger.With().Str(logAccountId, accountId)) if hasCriteria { - mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes([]string{accountId}, req.session, req.ctx, logger, filter) + mailboxesByAccountId, sessionState, lang, err := g.jmap.SearchMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language(), filter) if err != nil { return req.errorResponseFromJmap(err) } mailboxes, ok := mailboxesByAccountId[accountId] if ok { - return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State) + return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State, lang) } else { return notFoundResponse(sessionState) } } else { - mailboxesByAccountId, sessionState, err := g.jmap.GetAllMailboxes([]string{accountId}, req.session, req.ctx, logger) + mailboxesByAccountId, sessionState, lang, err := g.jmap.GetAllMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language()) if err != nil { return req.errorResponseFromJmap(err) } mailboxes, ok := mailboxesByAccountId[accountId] if ok { - return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State) + return etagResponse(mailboxes.Mailboxes, sessionState, mailboxes.State, lang) } else { return notFoundResponse(sessionState) } @@ -193,17 +193,17 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds))) if hasCriteria { - mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, filter) + mailboxesByAccountId, sessionState, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter) if err != nil { return req.errorResponseFromJmap(err) } - return response(mailboxesByAccountId, sessionState) + return response(mailboxesByAccountId, sessionState, lang) } else { - mailboxesByAccountId, sessionState, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger) + mailboxesByAccountId, sessionState, lang, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger, req.language()) if err != nil { return req.errorResponseFromJmap(err) } - return response(mailboxesByAccountId, sessionState) + return response(mailboxesByAccountId, sessionState, lang) } }) } @@ -221,11 +221,11 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt Role: role, } - mailboxesByAccountId, sessionState, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, filter) + mailboxesByAccountId, sessionState, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter) if err != nil { return req.errorResponseFromJmap(err) } - return response(mailboxesByAccountId, sessionState) + return response(mailboxesByAccountId, sessionState, lang) }) } @@ -267,12 +267,12 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) { logger := log.From(l) - changes, sessionState, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, mailboxId, sinceState, true, g.maxBodyValueBytes, maxChanges) + changes, sessionState, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.maxBodyValueBytes, maxChanges) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(changes, sessionState, changes.State) + return etagResponse(changes, sessionState, changes.State, lang) }) } @@ -320,12 +320,12 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht logger := log.From(l) - changesByAccountId, sessionState, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, sinceStateMap, true, g.maxBodyValueBytes, maxChanges) + changesByAccountId, sessionState, lang, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language(), sinceStateMap, true, g.maxBodyValueBytes, maxChanges) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return response(changesByAccountId, sessionState) + return response(changesByAccountId, sessionState, lang) }) } @@ -336,11 +336,11 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) { l.Array(logAccountId, log.SafeStringArray(allAccountIds)) logger := log.From(l) - rolesByAccountId, sessionState, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger) + rolesByAccountId, sessionState, lang, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return response(rolesByAccountId, sessionState) + return response(rolesByAccountId, sessionState, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_api_quota.go b/services/groupware/pkg/groupware/groupware_api_quota.go new file mode 100644 index 000000000..eebf6c148 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_api_quota.go @@ -0,0 +1,39 @@ +package groupware + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// When the request succeeds. +// swagger:response GetQuotaResponse200 +type SwaggerGetQuotaResponse200 struct { + // in: body + Body []jmap.Quota +} + +// swagger:route GET /groupware/accounts/{account}/quota quota getquota +// Get quota limits. +// +// responses: +// +// 200: GetQuotaResponse200 +// 400: ErrorResponse400 +// 500: ErrorResponse500 +func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + accountId, err := req.GetAccountIdForQuota() + if err != nil { + return errorResponse(err) + } + logger := log.From(req.logger.With().Str(logAccountId, accountId)) + + res, sessionState, lang, jerr := g.jmap.GetQuotas(accountId, req.session, req.ctx, logger, req.language()) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + return etagResponse(res.List, sessionState, res.State, lang) + }) +} diff --git a/services/groupware/pkg/groupware/groupware_api_tasklists.go b/services/groupware/pkg/groupware/groupware_api_tasklists.go index e1b9511f7..4ffb6bc80 100644 --- a/services/groupware/pkg/groupware/groupware_api_tasklists.go +++ b/services/groupware/pkg/groupware/groupware_api_tasklists.go @@ -31,7 +31,7 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) { } var _ string = accountId - return response(AllTaskLists, req.session.State) + return response(AllTaskLists, req.session.State, "") }) } @@ -65,7 +65,7 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) { // TODO replace with proper implementation for _, tasklist := range AllTaskLists { if tasklist.Id == tasklistId { - return response(tasklist, req.session.State) + return response(tasklist, req.session.State, "") } } return notFoundResponse(req.session.State) @@ -102,6 +102,6 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) { if !ok { return notFoundResponse(req.session.State) } - return response(tasks, req.session.State) + return response(tasks, req.session.State, "") }) } diff --git a/services/groupware/pkg/groupware/groupware_api_vacation.go b/services/groupware/pkg/groupware/groupware_api_vacation.go index 9a229aca6..a8b86bef3 100644 --- a/services/groupware/pkg/groupware/groupware_api_vacation.go +++ b/services/groupware/pkg/groupware/groupware_api_vacation.go @@ -37,11 +37,11 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) { } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - res, sessionState, jerr := g.jmap.GetVacationResponse(accountId, req.session, req.ctx, logger) + res, sessionState, lang, jerr := g.jmap.GetVacationResponse(accountId, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(res, sessionState, res.State) + return etagResponse(res, sessionState, res.State, lang) }) } @@ -79,11 +79,11 @@ func (g *Groupware) SetVacation(w http.ResponseWriter, r *http.Request) { } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - res, sessionState, jerr := g.jmap.SetVacationResponse(accountId, body, req.session, req.ctx, logger) + res, sessionState, lang, jerr := g.jmap.SetVacationResponse(accountId, body, req.session, req.ctx, logger, req.language()) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(res, sessionState, res.ResponseState) + return etagResponse(res, sessionState, res.ResponseState, lang) }) } diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index 8f475906b..63d1d6547 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -575,6 +575,10 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons w.Header().Add("Session-State", string(sessionState)) } + if response.contentLanguage != "" { + w.Header().Add("Content-Language", string(response.contentLanguage)) + } + notModified := false if etag != "" { challenge := r.Header.Get("if-none-match") diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go index 7005d5040..89034c2bb 100644 --- a/services/groupware/pkg/groupware/groupware_request.go +++ b/services/groupware/pkg/groupware/groupware_request.go @@ -65,8 +65,8 @@ var ( errNoPrimaryAccountForTask = errors.New("no primary account for task") errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar") errNoPrimaryAccountForContact = errors.New("no primary account for contact") + errNoPrimaryAccountForQuota = errors.New("no primary account for quota") // errNoPrimaryAccountForSieve = errors.New("no primary account for sieve") - // errNoPrimaryAccountForQuota = errors.New("no primary account for quota") // errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket") ) @@ -109,6 +109,10 @@ func (r Request) GetAccountIdForVacationResponse() (string, *Error) { return r.getAccountId(r.session.PrimaryAccounts.VacationResponse, errNoPrimaryAccountForVacationResponse) } +func (r Request) GetAccountIdForQuota() (string, *Error) { + return r.getAccountId(r.session.PrimaryAccounts.Quota, errNoPrimaryAccountForQuota) +} + func (r Request) GetAccountIdForSubmission() (string, *Error) { return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission) } @@ -280,6 +284,10 @@ func (r Request) body(target any) *Error { return nil } +func (r Request) language() string { + return r.r.Header.Get("Accept-Language") +} + func (r Request) observe(obs prometheus.Observer, value float64) { metrics.WithExemplar(obs, value, r.GetRequestId(), r.GetTraceId()) } diff --git a/services/groupware/pkg/groupware/groupware_response.go b/services/groupware/pkg/groupware/groupware_response.go index d380fa9e4..49456c2fb 100644 --- a/services/groupware/pkg/groupware/groupware_response.go +++ b/services/groupware/pkg/groupware/groupware_response.go @@ -7,11 +7,12 @@ import ( ) type Response struct { - body any - status int - err *Error - etag jmap.State - sessionState jmap.SessionState + body any + status int + err *Error + etag jmap.State + sessionState jmap.SessionState + contentLanguage jmap.Language } func errorResponse(err *Error) Response { @@ -32,30 +33,33 @@ func errorResponseWithSessionState(err *Error, sessionState jmap.SessionState) R } } -func response(body any, sessionState jmap.SessionState) Response { +func response(body any, sessionState jmap.SessionState, contentLanguage jmap.Language) Response { return Response{ - body: body, - err: nil, - etag: jmap.State(sessionState), - sessionState: sessionState, + body: body, + err: nil, + etag: jmap.State(sessionState), + sessionState: sessionState, + contentLanguage: contentLanguage, } } -func etagResponse(body any, sessionState jmap.SessionState, etag jmap.State) Response { +func etagResponse(body any, sessionState jmap.SessionState, etag jmap.State, contentLanguage jmap.Language) Response { return Response{ - body: body, - err: nil, - etag: etag, - sessionState: sessionState, + body: body, + err: nil, + etag: etag, + sessionState: sessionState, + contentLanguage: contentLanguage, } } -func etagOnlyResponse(body any, etag jmap.State) Response { +func etagOnlyResponse(body any, etag jmap.State, contentLanguage jmap.Language) Response { return Response{ - body: body, - err: nil, - etag: etag, - sessionState: "", + body: body, + err: nil, + etag: etag, + sessionState: "", + contentLanguage: contentLanguage, } } diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 8a49cc1bc..70af31403 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -72,6 +72,7 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/identities", g.GetIdentities) r.Get("/vacation", g.GetVacation) r.Put("/vacation", g.SetVacation) + r.Get("/quota", g.GetQuota) r.Route("/mailboxes", func(r chi.Router) { r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed= r.Get("/{mailbox}", g.GetMailbox)