groupware: add quota API + add support for Accept-Language and Content-Language

This commit is contained in:
Pascal Bleser
2025-10-06 11:58:36 +02:00
parent 36cf2c323e
commit deee6102ec
29 changed files with 362 additions and 237 deletions

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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{}

View File

@@ -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

View File

@@ -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
})
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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{} },
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)
})
}

View File

@@ -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 {

View File

@@ -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, "")
})
}

View File

@@ -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, "")
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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)
})
}

View File

@@ -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, "")
})
}

View File

@@ -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)
})
}

View File

@@ -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")

View File

@@ -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())
}

View File

@@ -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,
}
}

View File

@@ -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)