From 3c19e1d1dbbdb524bf3b51589a2b251c3ea9b881 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Tue, 28 Oct 2025 17:53:45 +0100
Subject: [PATCH] groupware: implement Mailbox modification endpoints +
refactor ETag/state in the framework
* add endpoints for Mailboxes:
- PATCH mailboxes/{id}
- DELETE mailboxes/{id}
- POST mailboxes
* refactor the pkg/jmap and groupware framework to systematically
return a jmap.State out-of-band of the per-method payloads, since
they are almost always present in JMAP responses, which lead to the
artificial creation of a lot of composed struct types just to also
return the State; on the downside, it adds yet another return
parameter
---
pkg/jmap/jmap_api_blob.go | 37 ++-
pkg/jmap/jmap_api_bootstrap.go | 17 +-
pkg/jmap/jmap_api_calendar.go | 10 +-
pkg/jmap/jmap_api_contact.go | 68 +++---
pkg/jmap/jmap_api_email.go | 214 ++++++++----------
pkg/jmap/jmap_api_identity.go | 110 ++++-----
pkg/jmap/jmap_api_mailbox.go | 207 ++++++++++++-----
pkg/jmap/jmap_api_quota.go | 10 +-
pkg/jmap/jmap_api_vacation.go | 34 ++-
pkg/jmap/jmap_error.go | 1 +
pkg/jmap/jmap_integration_test.go | 22 +-
pkg/jmap/jmap_model.go | 117 +++++++++-
pkg/jmap/jmap_test.go | 6 +-
pkg/jmap/jmap_tools.go | 86 ++++++-
pkg/jmap/jmap_tools_test.go | 2 +-
.../pkg/groupware/groupware_api_blob.go | 11 +-
.../pkg/groupware/groupware_api_calendars.go | 4 +-
.../pkg/groupware/groupware_api_contacts.go | 22 +-
.../pkg/groupware/groupware_api_emails.go | 152 ++++++-------
.../pkg/groupware/groupware_api_identity.go | 28 ++-
.../pkg/groupware/groupware_api_index.go | 6 +-
.../pkg/groupware/groupware_api_mailbox.go | 136 +++++++++--
.../pkg/groupware/groupware_api_quota.go | 8 +-
.../pkg/groupware/groupware_api_vacation.go | 10 +-
.../pkg/groupware/groupware_route.go | 11 +-
.../groupware/pkg/groupware/groupware_test.go | 3 +-
26 files changed, 801 insertions(+), 531 deletions(-)
diff --git a/pkg/jmap/jmap_api_blob.go b/pkg/jmap/jmap_api_blob.go
index 8c1c37ac30..f3561992d5 100644
--- a/pkg/jmap/jmap_api_blob.go
+++ b/pkg/jmap/jmap_api_blob.go
@@ -9,12 +9,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
)
-type BlobResponse struct {
- Blob *Blob `json:"blob,omitempty"`
- State State `json:"state,omitempty"`
-}
-
-func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (BlobResponse, SessionState, Language, Error) {
+func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (*Blob, SessionState, State, Language, Error) {
cmd, jerr := j.request(session, logger,
invocation(CommandBlobGet, BlobGetCommand{
AccountId: accountId,
@@ -24,22 +19,22 @@ func (j *Client) GetBlobMetadata(accountId string, session *Session, ctx context
}, "0"),
)
if jerr != nil {
- return BlobResponse{}, "", "", jerr
+ return nil, "", "", "", jerr
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (BlobResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*Blob, State, Error) {
var response BlobGetResponse
err := retrieveResponseMatchParameters(logger, body, CommandBlobGet, "0", &response)
if err != nil {
- return BlobResponse{}, err
+ return nil, "", err
}
if len(response.List) != 1 {
logger.Error().Msgf("%T.List has %v entries instead of 1", response, len(response.List))
- return BlobResponse{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return nil, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
get := response.List[0]
- return BlobResponse{Blob: &get, State: response.State}, nil
+ return &get, response.State, nil
})
}
@@ -48,7 +43,6 @@ type UploadedBlob struct {
Size int `json:"size"`
Type string `json:"type"`
Sha512 string `json:"sha:512"`
- State State `json:"state"`
}
func (j *Client) UploadBlobStream(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, contentType string, body io.Reader) (UploadedBlob, Language, Error) {
@@ -70,7 +64,7 @@ func (j *Client) DownloadBlobStream(accountId string, blobId string, name string
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, acceptLanguage string, data []byte, contentType string) (UploadedBlob, SessionState, Language, Error) {
+func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte, contentType string) (UploadedBlob, SessionState, State, Language, Error) {
encoded := base64.StdEncoding.EncodeToString(data)
upload := BlobUploadCommand{
@@ -100,35 +94,35 @@ 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, acceptLanguage, func(body *Response) (UploadedBlob, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UploadedBlob, State, Error) {
var uploadResponse BlobUploadResponse
err := retrieveResponseMatchParameters(logger, body, CommandBlobUpload, "0", &uploadResponse)
if err != nil {
- return UploadedBlob{}, err
+ return UploadedBlob{}, "", err
}
var getResponse BlobGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandBlobGet, "1", &getResponse)
if err != nil {
- return UploadedBlob{}, err
+ return UploadedBlob{}, "", err
}
if len(uploadResponse.Created) != 1 {
logger.Error().Msgf("%T.Created has %v entries instead of 1", uploadResponse, len(uploadResponse.Created))
- return UploadedBlob{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedBlob{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
upload, ok := uploadResponse.Created["0"]
if !ok {
logger.Error().Msgf("%T.Created has no item '0'", uploadResponse)
- return UploadedBlob{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedBlob{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
if len(getResponse.List) != 1 {
logger.Error().Msgf("%T.List has %v entries instead of 1", getResponse, len(getResponse.List))
- return UploadedBlob{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedBlob{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
get := getResponse.List[0]
@@ -137,8 +131,7 @@ func (j *Client) UploadBlob(accountId string, session *Session, ctx context.Cont
Size: upload.Size,
Type: upload.Type,
Sha512: get.DigestSha512,
- State: getResponse.State,
- }, nil
+ }, getResponse.State, nil
})
}
diff --git a/pkg/jmap/jmap_api_bootstrap.go b/pkg/jmap/jmap_api_bootstrap.go
index 70bb7987fe..2635bf273a 100644
--- a/pkg/jmap/jmap_api_bootstrap.go
+++ b/pkg/jmap/jmap_api_bootstrap.go
@@ -12,7 +12,7 @@ type AccountBootstrapResult struct {
Quotas []Quota `json:"quotas,omitempty"`
}
-func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AccountBootstrapResult, SessionState, Language, Error) {
+func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AccountBootstrapResult, SessionState, State, Language, Error) {
uniqueAccountIds := structs.Uniq(accountIds)
logger = j.logger("GetBootstrap", session, logger)
@@ -25,26 +25,30 @@ func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context
cmd, err := j.request(session, logger, calls...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]AccountBootstrapResult, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]AccountBootstrapResult, State, Error) {
identityPerAccount := map[string][]Identity{}
quotaPerAccount := map[string][]Quota{}
+ identityStatesPerAccount := map[string]State{}
+ quotaStatesPerAccount := map[string]State{}
for _, accountId := range uniqueAccountIds {
var identityResponse IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, mcid(accountId, "I"), &identityResponse)
if err != nil {
- return nil, err
+ return nil, "", err
} else {
identityPerAccount[accountId] = identityResponse.List
+ identityStatesPerAccount[accountId] = identityResponse.State
}
var quotaResponse QuotaGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandQuotaGet, mcid(accountId, "Q"), "aResponse)
if err != nil {
- return nil, err
+ return nil, "", err
} else {
quotaPerAccount[accountId] = quotaResponse.List
+ quotaStatesPerAccount[accountId] = quotaResponse.State
}
}
@@ -65,6 +69,7 @@ func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context
r.Quotas = value
result[accountId] = r
}
- return result, nil
+
+ return result, squashStateMaps(identityStatesPerAccount, quotaStatesPerAccount), nil
})
}
diff --git a/pkg/jmap/jmap_api_calendar.go b/pkg/jmap/jmap_api_calendar.go
index f2fffc24a3..6f57205077 100644
--- a/pkg/jmap/jmap_api_calendar.go
+++ b/pkg/jmap/jmap_api_calendar.go
@@ -6,22 +6,22 @@ import (
"github.com/opencloud-eu/opencloud/pkg/log"
)
-func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, blobIds []string) (CalendarEventParseResponse, SessionState, Language, Error) {
+func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, blobIds []string) (CalendarEventParseResponse, SessionState, State, Language, Error) {
logger = j.logger("ParseICalendarBlob", session, logger)
cmd, err := j.request(session, logger,
invocation(CommandCalendarEventParse, CalendarEventParseCommand{AccountId: accountId, BlobIDs: blobIds}, "0"),
)
if err != nil {
- return CalendarEventParseResponse{}, "", "", err
+ return CalendarEventParseResponse{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CalendarEventParseResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CalendarEventParseResponse, State, Error) {
var response CalendarEventParseResponse
err = retrieveResponseMatchParameters(logger, body, CommandCalendarEventParse, "0", &response)
if err != nil {
- return CalendarEventParseResponse{}, err
+ return CalendarEventParseResponse{}, "", err
}
- return response, nil
+ return response, "", nil
})
}
diff --git a/pkg/jmap/jmap_api_contact.go b/pkg/jmap/jmap_api_contact.go
index e5f4357136..7e5e92fa7f 100644
--- a/pkg/jmap/jmap_api_contact.go
+++ b/pkg/jmap/jmap_api_contact.go
@@ -12,36 +12,34 @@ import (
type AddressBooksResponse struct {
AddressBooks []AddressBook `json:"addressbooks"`
NotFound []string `json:"notFound,omitempty"`
- State State `json:"state"`
}
-func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, Language, Error) {
+func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, State, Language, Error) {
logger = j.logger("GetAddressbooks", session, logger)
cmd, err := j.request(session, logger,
invocation(CommandAddressBookGet, AddressBookGetCommand{AccountId: accountId, Ids: ids}, "0"),
)
if err != nil {
- return AddressBooksResponse{}, "", "", err
+ return AddressBooksResponse{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (AddressBooksResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (AddressBooksResponse, State, Error) {
var response AddressBookGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandAddressBookGet, "0", &response)
if err != nil {
- return AddressBooksResponse{}, err
+ return AddressBooksResponse{}, response.State, err
}
return AddressBooksResponse{
AddressBooks: response.List,
NotFound: response.NotFound,
- State: response.State,
- }, nil
+ }, response.State, nil
})
}
func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string,
filter ContactCardFilterElement, sortBy []ContactCardComparator,
- position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, Language, Error) {
+ position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) {
logger = j.logger("QueryContactCards", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
@@ -75,32 +73,29 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]jscontact.ContactCard, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]jscontact.ContactCard, State, Error) {
resp := map[string][]jscontact.ContactCard{}
+ stateByAccountId := map[string]State{}
for _, accountId := range uniqueAccountIds {
var response ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, mcid(accountId, "1"), &response)
if err != nil {
- return nil, err
+ return nil, "", err
}
if len(response.NotFound) > 0 {
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
}
resp[accountId] = response.List
+ stateByAccountId[accountId] = response.State
}
- return resp, nil
+ return resp, squashState(stateByAccountId), nil
})
}
-type CreatedContactCard struct {
- ContactCard *jscontact.ContactCard `json:"contactCard"`
- State State `json:"state"`
-}
-
-func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (CreatedContactCard, SessionState, Language, Error) {
+func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (*jscontact.ContactCard, SessionState, State, Language, Error) {
logger = j.logger("CreateContactCard", session, logger)
cmd, err := j.request(session, logger,
@@ -116,53 +111,45 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte
}, "1"),
)
if err != nil {
- return CreatedContactCard{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CreatedContactCard, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*jscontact.ContactCard, State, Error) {
var setResponse ContactCardSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
if err != nil {
- return CreatedContactCard{}, err
+ return nil, "", err
}
setErr, notok := setResponse.NotCreated["c"]
if notok {
logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr)
- return CreatedContactCard{}, setErrorError(setErr, EmailType)
+ return nil, "", setErrorError(setErr, EmailType)
}
if created, ok := setResponse.Created["c"]; !ok || created == nil {
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
logger.Error().Err(berr)
- return CreatedContactCard{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
+ return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
}
var getResponse ContactCardGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "1", &getResponse)
if err != nil {
- return CreatedContactCard{}, err
+ return nil, "", err
}
if len(getResponse.List) < 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
logger.Error().Err(berr)
- return CreatedContactCard{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
+ return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
}
- return CreatedContactCard{
- ContactCard: &getResponse.List[0],
- State: setResponse.NewState,
- }, nil
+ return &getResponse.List[0], setResponse.NewState, nil
})
}
-type DeletedContactCards struct {
- State State `json:"state"`
- NotDestroyed map[string]SetError `json:"notDestroyed"`
-}
-
-func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (DeletedContactCards, SessionState, Language, Error) {
+func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
logger = j.logger("DeleteContactCard", session, logger)
cmd, err := j.request(session, logger,
@@ -172,18 +159,15 @@ func (j *Client) DeleteContactCard(accountId string, destroy []string, session *
}, "0"),
)
if err != nil {
- return DeletedContactCards{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (DeletedContactCards, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) {
var setResponse ContactCardSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
if err != nil {
- return DeletedContactCards{}, err
+ return nil, "", err
}
- return DeletedContactCards{
- State: setResponse.NewState,
- NotDestroyed: setResponse.NotDestroyed,
- }, nil
+ return setResponse.NotDestroyed, setResponse.NewState, nil
})
}
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index 54d571072e..c490af6e2c 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -16,11 +16,10 @@ type Emails struct {
Total uint `json:"total,omitzero"`
Limit uint `json:"limit,omitzero"`
Offset uint `json:"offset,omitzero"`
- State State `json:"state,omitempty"`
}
// Retrieve specific Emails by their id.
-func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) (Emails, SessionState, Language, Error) {
+func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) ([]Email, SessionState, State, Language, Error) {
logger = j.logger("GetEmails", session, logger)
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
@@ -53,62 +52,62 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
logger.Error().Err(err).Send()
- return Emails{}, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ return nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Email, State, Error) {
if markAsSeen {
var markResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &markResponse)
if err != nil {
- return Emails{}, err
+ return nil, "", err
}
for _, seterr := range markResponse.NotUpdated {
// TODO we don't have a way to compose multiple set errors yet
- return Emails{}, setErrorError(seterr, EmailType)
+ return nil, "", setErrorError(seterr, EmailType)
}
}
var response EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &response)
if err != nil {
- return Emails{}, err
+ return nil, "", err
}
if withThreads {
var threads ThreadGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, "2", &threads)
if err != nil {
- return Emails{}, err
+ return nil, "", err
}
setThreadSize(&threads, response.List)
}
- return Emails{Emails: response.List, State: response.State}, nil
+ return response.List, response.State, nil
})
}
-func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (string, SessionState, Language, Error) {
+func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (string, SessionState, State, Language, Error) {
logger = j.logger("GetEmailBlobId", session, logger)
get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}}
cmd, err := j.request(session, logger, invocation(CommandEmailGet, get, "0"))
if err != nil {
logger.Error().Err(err).Send()
- return "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ return "", "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (string, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (string, State, Error) {
var response EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "0", &response)
if err != nil {
- return "", err
+ return "", "", err
}
if len(response.List) != 1 {
- return "", nil
+ return "", "", nil
}
email := response.List[0]
- return email.BlobId, nil
+ return email.BlobId, response.State, nil
})
}
// 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, acceptLanguage string, mailboxId string, offset uint, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool) (Emails, SessionState, Language, Error) {
+func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, offset uint, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool) (Emails, SessionState, State, 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)
})
@@ -155,27 +154,27 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return Emails{}, "", "", err
+ return Emails{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Emails, State, Error) {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse)
if err != nil {
- return Emails{}, err
+ return Emails{}, "", err
}
var getResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &getResponse)
if err != nil {
logger.Error().Err(err).Send()
- return Emails{}, err
+ return Emails{}, "", err
}
if withThreads {
var thread ThreadGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, "2", &thread)
if err != nil {
- return Emails{}, err
+ return Emails{}, "", err
}
setThreadSize(&thread, getResponse.List)
}
@@ -185,12 +184,12 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c
Total: queryResponse.Total,
Limit: queryResponse.Limit,
Offset: queryResponse.Position,
- }, nil
+ }, queryResponse.QueryState, nil
})
}
// 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, acceptLanguage string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Language, 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, State, Language, Error) {
logger = j.loggerParams("GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
@@ -226,28 +225,28 @@ 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, acceptLanguage, func(body *Response) (MailboxChanges, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, State, Error) {
var changesResponse EmailChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailChanges, "0", &changesResponse)
if err != nil {
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err).Send()
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
return MailboxChanges{
@@ -256,8 +255,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
NewState: changesResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
- State: updatedResponse.State,
- }, nil
+ }, updatedResponse.State, nil
})
}
@@ -275,7 +273,7 @@ type EmailSnippetQueryResult struct {
QueryState State `json:"queryState"`
}
-func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint) (map[string]EmailSnippetQueryResult, SessionState, Language, Error) {
+func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint) (map[string]EmailSnippetQueryResult, SessionState, State, Language, Error) {
logger = j.loggerParams("QueryEmailSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Uint(logLimit, limit).Uint(logOffset, offset)
})
@@ -326,28 +324,28 @@ func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterEleme
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailSnippetQueryResult, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailSnippetQueryResult, State, Error) {
results := make(map[string]EmailSnippetQueryResult, len(uniqueAccountIds))
for _, accountId := range uniqueAccountIds {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var mailResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &mailResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var snippetResponse SearchSnippetGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, mcid(accountId, "2"), &snippetResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
mailResponseById := structs.Index(mailResponse.List, func(e Email) string { return e.Id })
@@ -379,7 +377,7 @@ func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterEleme
QueryState: queryResponse.QueryState,
}
}
- return results, nil
+ return results, squashStateFunc(results, func(r EmailSnippetQueryResult) State { return r.QueryState }), nil
})
}
@@ -391,7 +389,7 @@ type EmailQueryResult struct {
QueryState State `json:"queryState"`
}
-func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryResult, SessionState, Language, Error) {
+func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryResult, SessionState, State, Language, Error) {
logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
})
@@ -430,22 +428,22 @@ func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, ses
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryResult, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryResult, State, Error) {
results := make(map[string]EmailQueryResult, len(uniqueAccountIds))
for _, accountId := range uniqueAccountIds {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var emailsResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &emailsResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
results[accountId] = EmailQueryResult{
@@ -456,7 +454,7 @@ func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, ses
QueryState: queryResponse.QueryState,
}
}
- return results, nil
+ return results, squashStateFunc(results, func(r EmailQueryResult) State { return r.QueryState }), nil
})
}
@@ -473,7 +471,7 @@ type EmailQueryWithSnippetsResult struct {
QueryState State `json:"queryState"`
}
-func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryWithSnippetsResult, SessionState, Language, Error) {
+func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryWithSnippetsResult, SessionState, State, Language, Error) {
logger = j.loggerParams("QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies)
})
@@ -523,28 +521,28 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilter
cmd, err := j.request(session, logger, invocations...)
if err != nil {
logger.Error().Err(err).Send()
- return nil, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
+ return nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryWithSnippetsResult, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryWithSnippetsResult, State, Error) {
result := make(map[string]EmailQueryWithSnippetsResult, len(uniqueAccountIds))
for _, accountId := range uniqueAccountIds {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var snippetResponse SearchSnippetGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, mcid(accountId, "1"), &snippetResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var emailsResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "2"), &emailsResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
snippetsById := map[string][]SearchSnippet{}
@@ -576,7 +574,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilter
QueryState: queryResponse.QueryState,
}
}
- return result, nil
+ return result, squashStateFunc(result, func(r EmailQueryWithSnippetsResult) State { return r.QueryState }), nil
})
}
@@ -587,7 +585,7 @@ type UploadedEmail struct {
Sha512 string `json:"sha:512"`
}
-func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte) (UploadedEmail, SessionState, Language, Error) {
+func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte) (UploadedEmail, SessionState, State, Language, Error) {
encoded := base64.StdEncoding.EncodeToString(data)
upload := BlobUploadCommand{
@@ -617,36 +615,36 @@ 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, acceptLanguage, func(body *Response) (UploadedEmail, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UploadedEmail, State, Error) {
var uploadResponse BlobUploadResponse
err = retrieveResponseMatchParameters(logger, body, CommandBlobUpload, "0", &uploadResponse)
if err != nil {
- return UploadedEmail{}, err
+ return UploadedEmail{}, "", err
}
var getResponse BlobGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandBlobGet, "1", &getResponse)
if err != nil {
logger.Error().Err(err).Send()
- return UploadedEmail{}, err
+ return UploadedEmail{}, "", err
}
if len(uploadResponse.Created) != 1 {
logger.Error().Msgf("%T.Created has %v elements instead of 1", uploadResponse, len(uploadResponse.Created))
- return UploadedEmail{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
upload, ok := uploadResponse.Created["0"]
if !ok {
logger.Error().Msgf("%T.Created has no element '0'", uploadResponse)
- return UploadedEmail{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
if len(getResponse.List) != 1 {
logger.Error().Msgf("%T.List has %v elements instead of 1", getResponse, len(getResponse.List))
- return UploadedEmail{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
+ return UploadedEmail{}, "", simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
get := getResponse.List[0]
@@ -655,17 +653,12 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
Size: upload.Size,
Type: upload.Type,
Sha512: get.DigestSha512,
- }, nil
+ }, State(get.DigestSha256), nil
})
}
-type CreatedEmail struct {
- Email *Email `json:"email"`
- State State `json:"state"`
-}
-
-func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (CreatedEmail, SessionState, Language, Error) {
+func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (*Email, SessionState, State, Language, Error) {
set := EmailSetCommand{
AccountId: accountId,
Create: map[string]EmailCreate{
@@ -680,14 +673,14 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri
invocation(CommandEmailSet, set, "0"),
)
if err != nil {
- return CreatedEmail{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CreatedEmail, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*Email, State, Error) {
var setResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse)
if err != nil {
- return CreatedEmail{}, err
+ return nil, "", err
}
if len(setResponse.NotCreated) > 0 {
@@ -698,28 +691,20 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, replaceId stri
setErr, notok := setResponse.NotCreated["c"]
if notok {
logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr)
- return CreatedEmail{}, setErrorError(setErr, EmailType)
+ return nil, "", setErrorError(setErr, EmailType)
}
created, ok := setResponse.Created["c"]
if !ok {
berr := fmt.Errorf("failed to find %s in %s response", string(EmailType), string(CommandEmailSet))
logger.Error().Err(berr)
- return CreatedEmail{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
+ return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
}
- return CreatedEmail{
- Email: created,
- State: setResponse.NewState,
- }, nil
+ return created, setResponse.NewState, nil
})
}
-type UpdatedEmails struct {
- Updated map[string]*Email `json:"email"`
- State State `json:"state"`
-}
-
// The Email/set method encompasses:
// - Changing the keywords of an Email (e.g., unread/flagged status)
// - Adding/removing an Email to/from Mailboxes (moving a message)
@@ -728,7 +713,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, acceptLanguage string) (UpdatedEmails, SessionState, Language, Error) {
+func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]*Email, SessionState, State, Language, Error) {
cmd, err := j.request(session, logger,
invocation(CommandEmailSet, EmailSetCommand{
AccountId: accountId,
@@ -736,32 +721,24 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
}, "0"),
)
if err != nil {
- return UpdatedEmails{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (UpdatedEmails, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]*Email, State, Error) {
var setResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse)
if err != nil {
- return UpdatedEmails{}, err
+ return nil, "", err
}
if len(setResponse.NotUpdated) != len(updates) {
// error occured
// TODO(pbleser-oc) handle submission errors
}
- return UpdatedEmails{
- Updated: setResponse.Updated,
- State: setResponse.NewState,
- }, nil
+ return setResponse.Updated, setResponse.NewState, nil
})
}
-type DeletedEmails struct {
- State State `json:"state"`
- NotDestroyed map[string]SetError `json:"notDestroyed"`
-}
-
-func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (DeletedEmails, SessionState, Language, Error) {
+func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
cmd, err := j.request(session, logger,
invocation(CommandEmailSet, EmailSetCommand{
AccountId: accountId,
@@ -769,25 +746,21 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi
}, "0"),
)
if err != nil {
- return DeletedEmails{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (DeletedEmails, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) {
var setResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse)
if err != nil {
- return DeletedEmails{}, err
+ return nil, "", err
}
- return DeletedEmails{
- State: setResponse.NewState,
- NotDestroyed: setResponse.NotDestroyed,
- }, nil
+ return setResponse.NotDestroyed, setResponse.NewState, nil
})
}
type SubmittedEmail struct {
Id string `json:"id"`
- State State `json:"state"`
SendAt time.Time `json:"sendAt,omitzero"`
ThreadId string `json:"threadId,omitempty"`
UndoStatus EmailSubmissionUndoStatus `json:"undoStatus,omitempty"`
@@ -810,7 +783,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, acceptLanguage string, data []byte) (SubmittedEmail, SessionState, Language, 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, State, Language, Error) {
set := EmailSubmissionSetCommand{
AccountId: accountId,
Create: map[string]EmailSubmissionCreate{
@@ -840,14 +813,14 @@ 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, acceptLanguage, func(body *Response) (SubmittedEmail, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (SubmittedEmail, State, Error) {
var submissionResponse EmailSubmissionSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionSet, "0", &submissionResponse)
if err != nil {
- return SubmittedEmail{}, err
+ return SubmittedEmail{}, "", err
}
if len(submissionResponse.NotCreated) > 0 {
@@ -863,13 +836,13 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
var setResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse)
if err != nil {
- return SubmittedEmail{}, err
+ return SubmittedEmail{}, "", err
}
var getResponse EmailSubmissionGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "1", &getResponse)
if err != nil {
- return SubmittedEmail{}, err
+ return SubmittedEmail{}, "", err
}
if len(getResponse.List) != 1 {
@@ -881,18 +854,17 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
return SubmittedEmail{
Id: submission.Id,
- State: setResponse.NewState,
SendAt: submission.SendAt,
ThreadId: submission.ThreadId,
UndoStatus: submission.UndoStatus,
Envelope: submission.Envelope,
DsnBlobIds: submission.DsnBlobIds,
MdnBlobIds: submission.MdnBlobIds,
- }, nil
+ }, setResponse.NewState, nil
})
}
-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) {
+func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, State, Language, Error) {
logger = j.loggerParams("EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
})
@@ -914,16 +886,16 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess
}, "1"),
)
if err != nil {
- return []Email{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Email, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Email, State, Error) {
var emailsResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &emailsResponse)
if err != nil {
- return []Email{}, err
+ return nil, "", err
}
- return emailsResponse.List, nil
+ return emailsResponse.List, emailsResponse.State, nil
})
}
@@ -954,7 +926,7 @@ var EmailSummaryProperties = []string{
EmailPropertyPreview,
}
-func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint, withThreads bool) (map[string]EmailsSummary, SessionState, Language, Error) {
+func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint, withThreads bool) (map[string]EmailsSummary, SessionState, State, Language, Error) {
logger = j.logger("QueryEmailSummaries", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
@@ -995,22 +967,22 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailsSummary, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailsSummary, State, Error) {
resp := map[string]EmailsSummary{}
for _, accountId := range uniqueAccountIds {
var queryResponse EmailQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
if err != nil {
- return nil, err
+ return nil, "", err
}
var response EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &response)
if err != nil {
- return nil, err
+ return nil, "", err
}
if len(response.NotFound) > 0 {
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
@@ -1019,7 +991,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
var thread ThreadGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, mcid(accountId, "2"), &thread)
if err != nil {
- return nil, err
+ return nil, "", err
}
setThreadSize(&thread, response.List)
}
@@ -1032,7 +1004,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
State: response.State,
}
}
- return resp, nil
+ return resp, squashStateFunc(resp, func(s EmailsSummary) State { return s.State }), nil
})
}
diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go
index 07d56353c9..3eabf7abf5 100644
--- a/pkg/jmap/jmap_api_identity.go
+++ b/pkg/jmap/jmap_api_identity.go
@@ -8,60 +8,46 @@ import (
"github.com/opencloud-eu/opencloud/pkg/structs"
)
-type Identities struct {
- Identities []Identity `json:"identities"`
- State State `json:"state"`
-}
-
-func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (Identities, SessionState, Language, Error) {
+func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]Identity, SessionState, State, Language, Error) {
logger = j.logger("GetAllIdentities", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, "0"))
if err != nil {
- return Identities{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identities, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Identity, State, Error) {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response)
if err != nil {
- return Identities{}, err
+ return nil, "", err
}
- return Identities{
- Identities: response.List,
- State: response.State,
- }, nil
+ return response.List, response.State, nil
})
}
-func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) (Identities, SessionState, Language, Error) {
+func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) ([]Identity, SessionState, State, Language, Error) {
logger = j.logger("GetIdentities", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId, Ids: identityIds}, "0"))
if err != nil {
- return Identities{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identities, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Identity, State, Error) {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response)
if err != nil {
- return Identities{}, err
+ return nil, "", err
}
- return Identities{
- Identities: response.List,
- State: response.State,
- }, nil
+ return response.List, response.State, nil
})
}
type IdentitiesGetResponse struct {
Identities map[string][]Identity `json:"identities,omitempty"`
NotFound []string `json:"notFound,omitempty"`
- State State `json:"state"`
}
-func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, Language, Error) {
- uniqueAccountIds := structs.Uniq(accountIds)
-
+func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, State, Language, Error) {
logger = j.logger("GetIdentitiesForAllAccounts", session, logger)
-
+ uniqueAccountIds := structs.Uniq(accountIds)
calls := make([]Invocation, len(uniqueAccountIds))
for i, accountId := range uniqueAccountIds {
calls[i] = invocation(CommandIdentityGet, IdentityGetCommand{AccountId: accountId}, strconv.Itoa(i))
@@ -69,40 +55,38 @@ func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Sessi
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, acceptLanguage, func(body *Response) (IdentitiesGetResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentitiesGetResponse, State, Error) {
identities := make(map[string][]Identity, len(uniqueAccountIds))
- var lastState State
+ stateByAccountId := make(map[string]State, len(uniqueAccountIds))
notFound := []string{}
for i, accountId := range uniqueAccountIds {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, strconv.Itoa(i), &response)
if err != nil {
- return IdentitiesGetResponse{}, err
+ return IdentitiesGetResponse{}, "", err
} else {
identities[accountId] = response.List
}
- lastState = response.State
+ stateByAccountId[accountId] = response.State
notFound = append(notFound, response.NotFound...)
}
return IdentitiesGetResponse{
Identities: identities,
NotFound: structs.Uniq(notFound),
- State: lastState,
- }, nil
+ }, squashState(stateByAccountId), nil
})
}
type IdentitiesAndMailboxesGetResponse struct {
Identities map[string][]Identity `json:"identities,omitempty"`
NotFound []string `json:"notFound,omitempty"`
- State State `json:"state"`
Mailboxes []Mailbox `json:"mailboxes"`
}
-func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesAndMailboxesGetResponse, SessionState, Language, Error) {
+func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesAndMailboxesGetResponse, SessionState, State, Language, Error) {
uniqueAccountIds := structs.Uniq(accountIds)
logger = j.logger("GetIdentitiesAndMailboxes", session, logger)
@@ -115,40 +99,39 @@ 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, acceptLanguage, func(body *Response) (IdentitiesAndMailboxesGetResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentitiesAndMailboxesGetResponse, State, Error) {
identities := make(map[string][]Identity, len(uniqueAccountIds))
- var lastState State
+ stateByAccountId := make(map[string]State, len(uniqueAccountIds))
notFound := []string{}
for i, accountId := range uniqueAccountIds {
var response IdentityGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, strconv.Itoa(i+1), &response)
if err != nil {
- return IdentitiesAndMailboxesGetResponse{}, err
+ return IdentitiesAndMailboxesGetResponse{}, "", err
} else {
identities[accountId] = response.List
}
- lastState = response.State
+ stateByAccountId[accountId] = response.State
notFound = append(notFound, response.NotFound...)
}
var mailboxResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "0", &mailboxResponse)
if err != nil {
- return IdentitiesAndMailboxesGetResponse{}, err
+ return IdentitiesAndMailboxesGetResponse{}, "", err
}
return IdentitiesAndMailboxesGetResponse{
Identities: identities,
NotFound: structs.Uniq(notFound),
- State: lastState,
Mailboxes: mailboxResponse.List,
- }, nil
+ }, squashState(stateByAccountId), nil
})
}
-func (j *Client) CreateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (State, SessionState, Language, Error) {
+func (j *Client) CreateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (Identity, SessionState, State, Language, Error) {
logger = j.logger("CreateIdentity", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{
AccountId: accountId,
@@ -157,24 +140,24 @@ func (j *Client) CreateIdentity(accountId string, session *Session, ctx context.
},
}, "0"))
if err != nil {
- return "", "", "", err
+ return Identity{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (State, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identity, State, Error) {
var response IdentitySetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response)
if err != nil {
- return response.NewState, err
+ return Identity{}, response.NewState, err
}
setErr, notok := response.NotCreated["c"]
if notok {
logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr)
- return "", setErrorError(setErr, IdentityType)
+ return Identity{}, "", setErrorError(setErr, IdentityType)
}
- return response.NewState, nil
+ return response.Created["c"], response.NewState, nil
})
}
-func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (State, SessionState, Language, Error) {
+func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (Identity, SessionState, State, Language, Error) {
logger = j.logger("UpdateIdentity", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{
AccountId: accountId,
@@ -183,48 +166,43 @@ func (j *Client) UpdateIdentity(accountId string, session *Session, ctx context.
},
}, "0"))
if err != nil {
- return "", "", "", err
+ return Identity{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (State, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identity, State, Error) {
var response IdentitySetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response)
if err != nil {
- return response.NewState, err
+ return Identity{}, response.NewState, err
}
setErr, notok := response.NotCreated["c"]
if notok {
logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr)
- return "", setErrorError(setErr, IdentityType)
+ return Identity{}, "", setErrorError(setErr, IdentityType)
}
- return response.NewState, nil
+ return response.Created["c"], response.NewState, nil
})
}
-type IdentityDeletion struct {
- Destroyed []string `json:"destroyed"`
- NewState State `json:"newState,omitempty"`
-}
-
-func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (IdentityDeletion, SessionState, Language, Error) {
+func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) ([]string, SessionState, State, Language, Error) {
logger = j.logger("DeleteIdentity", session, logger)
cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{
AccountId: accountId,
Destroy: ids,
}, "0"))
if err != nil {
- return IdentityDeletion{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (IdentityDeletion, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]string, State, Error) {
var response IdentitySetResponse
err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response)
if err != nil {
- return IdentityDeletion{}, err
+ return nil, "", err
}
for _, setErr := range response.NotDestroyed {
// TODO only returning the first error here, we should probably aggregate them instead
logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr)
- return IdentityDeletion{}, setErrorError(setErr, IdentityType)
+ return nil, "", setErrorError(setErr, IdentityType)
}
- return IdentityDeletion{Destroyed: response.Destroyed, NewState: response.NewState}, nil
+ return response.Destroyed, response.NewState, nil
})
}
diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go
index b9ee098c50..0f65f8e30a 100644
--- a/pkg/jmap/jmap_api_mailbox.go
+++ b/pkg/jmap/jmap_api_mailbox.go
@@ -2,6 +2,7 @@ package jmap
import (
"context"
+ "fmt"
"slices"
"github.com/opencloud-eu/opencloud/pkg/log"
@@ -12,41 +13,39 @@ import (
type MailboxesResponse struct {
Mailboxes []Mailbox `json:"mailboxes"`
NotFound []any `json:"notFound"`
- State State `json:"state"`
}
// https://jmap.io/spec-mail.html#mailboxget
-func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, Language, Error) {
+func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, State, 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, acceptLanguage, func(body *Response) (MailboxesResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxesResponse, State, Error) {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, "0", &response)
if err != nil {
- return MailboxesResponse{}, err
+ return MailboxesResponse{}, "", err
}
return MailboxesResponse{
Mailboxes: response.List,
NotFound: response.NotFound,
- State: response.State,
- }, nil
+ }, response.State, nil
})
}
-func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]Mailboxes, SessionState, Language, Error) {
+func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Mailbox, SessionState, State, Language, Error) {
logger = j.logger("GetAllMailboxes", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
- return nil, "", "", nil
+ return nil, "", "", "", nil
}
invocations := make([]Invocation, n)
@@ -56,35 +55,27 @@ func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx cont
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]Mailboxes, Error) {
- resp := map[string]Mailboxes{}
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]Mailbox, State, Error) {
+ resp := map[string][]Mailbox{}
+ stateByAccountid := map[string]State{}
for _, accountId := range uniqueAccountIds {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "0"), &response)
if err != nil {
- return nil, err
+ return nil, "", err
}
- resp[accountId] = Mailboxes{
- Mailboxes: response.List,
- State: response.State,
- }
+ resp[accountId] = response.List
+ stateByAccountid[accountId] = response.State
}
- return resp, nil
+ return resp, squashState(stateByAccountid), nil
})
}
-type Mailboxes struct {
- // The list of mailboxes that were found using the specified search criteria.
- Mailboxes []Mailbox `json:"mailboxes,omitempty"`
- // The state of the search.
- State State `json:"state,omitempty"`
-}
-
-func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter MailboxFilterElement) (map[string]Mailboxes, SessionState, Language, Error) {
+func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter MailboxFilterElement) (map[string][]Mailbox, SessionState, State, Language, Error) {
logger = j.logger("SearchMailboxes", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
@@ -103,21 +94,23 @@ 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 nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]Mailboxes, Error) {
- resp := map[string]Mailboxes{}
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]Mailbox, State, Error) {
+ resp := map[string][]Mailbox{}
+ stateByAccountid := map[string]State{}
for _, accountId := range uniqueAccountIds {
var response MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &response)
if err != nil {
- return map[string]Mailboxes{}, err
+ return nil, "", err
}
- resp[accountId] = Mailboxes{Mailboxes: response.List, State: response.State}
+ resp[accountId] = response.List
+ stateByAccountid[accountId] = response.State
}
- return resp, nil
+ return resp, squashState(stateByAccountid), nil
})
}
@@ -127,11 +120,10 @@ type MailboxChanges struct {
NewState State `json:"newState"`
Created []Email `json:"created,omitempty"`
Updated []Email `json:"updated,omitempty"`
- State State `json:"state,omitempty"`
}
// 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, acceptLanguage string, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Language, 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, State, Language, Error) {
logger = j.loggerParams("GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
@@ -167,28 +159,28 @@ 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, acceptLanguage, func(body *Response) (MailboxChanges, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (MailboxChanges, State, Error) {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, "0", &mailboxResponse)
if err != nil {
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err).Send()
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
- return MailboxChanges{}, err
+ return MailboxChanges{}, "", err
}
return MailboxChanges{
@@ -197,13 +189,12 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
- State: createdResponse.State,
- }, nil
+ }, createdResponse.State, nil
})
}
// Retrieve Email changes in Mailboxes of multiple Accounts.
-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) {
+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, State, Language, Error) {
logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context {
sinceStateLogDict := zerolog.Dict()
for k, v := range sinceStateMap {
@@ -215,7 +206,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)
@@ -257,28 +248,29 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return map[string]MailboxChanges{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]MailboxChanges, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]MailboxChanges, State, Error) {
resp := make(map[string]MailboxChanges, n)
+ stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxChanges, mcid(accountId, "0"), &mailboxResponse)
if err != nil {
- return map[string]MailboxChanges{}, err
+ return nil, "", err
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &createdResponse)
if err != nil {
- return map[string]MailboxChanges{}, err
+ return nil, "", err
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "2"), &updatedResponse)
if err != nil {
- return map[string]MailboxChanges{}, err
+ return nil, "", err
}
resp[accountId] = MailboxChanges{
@@ -287,21 +279,21 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
- State: createdResponse.State,
}
+ stateByAccountId[accountId] = createdResponse.State
}
- return resp, nil
+ return resp, squashState(stateByAccountId), nil
})
}
-func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]string, SessionState, Language, Error) {
+func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]string, SessionState, State, Language, Error) {
logger = j.logger("GetMailboxRolesForMultipleAccounts", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
- return map[string][]string{}, "", "", nil
+ return nil, "", "", "", nil
}
t := true
@@ -326,16 +318,17 @@ func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return map[string][]string{}, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]string, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]string, State, Error) {
resp := make(map[string][]string, n)
+ stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var getResponse MailboxGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "1"), &getResponse)
if err != nil {
- return map[string][]string{}, err
+ return nil, "", err
}
roles := make([]string, len(getResponse.List))
for i, mailbox := range getResponse.List {
@@ -343,18 +336,19 @@ func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, session
}
slices.Sort(roles)
resp[accountId] = roles
+ stateByAccountId[accountId] = getResponse.State
}
- return resp, nil
+ return resp, squashState(stateByAccountId), nil
})
}
-func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]string, SessionState, Language, Error) {
+func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]string, SessionState, State, Language, Error) {
logger = j.logger("GetInboxNameForMultipleAccounts", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
- return nil, "", "", nil
+ return nil, "", "", "", nil
}
invocations := make([]Invocation, n*2)
@@ -369,27 +363,116 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *S
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]string, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]string, State, Error) {
resp := make(map[string]string, n)
+ stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var r MailboxQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxGet, mcid(accountId, "0"), &r)
if err != nil {
- return nil, err
+ return nil, "", err
}
switch len(r.Ids) {
case 0:
// skip: account has no inbox?
case 1:
resp[accountId] = r.Ids[0]
+ stateByAccountId[accountId] = r.QueryState
default:
logger.Warn().Msgf("multiple ids for mailbox role='%v' for accountId='%v'", JmapMailboxRoleInbox, accountId)
resp[accountId] = r.Ids[0]
+ stateByAccountId[accountId] = r.QueryState
}
}
- return resp, nil
+ return resp, squashState(stateByAccountId), nil
+ })
+}
+
+func (j *Client) UpdateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, ifInState string, update MailboxChange) (Mailbox, SessionState, State, Language, Error) {
+ logger = j.logger("UpdateMailbox", session, logger)
+ cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{
+ AccountId: accountId,
+ IfInState: ifInState,
+ Update: map[string]PatchObject{
+ mailboxId: update.AsPatch(),
+ },
+ }, "0"))
+ if err != nil {
+ return Mailbox{}, "", "", "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Mailbox, State, Error) {
+ var setResp MailboxSetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxSet, "0", &setResp)
+ if err != nil {
+ return Mailbox{}, "", err
+ }
+ setErr, notok := setResp.NotUpdated["u"]
+ if notok {
+ logger.Error().Msgf("%T.NotUpdated returned an error %v", setResp, setErr)
+ return Mailbox{}, "", setErrorError(setErr, MailboxType)
+ }
+ return setResp.Updated["c"], setResp.NewState, nil
+ })
+}
+
+func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, create MailboxChange) (Mailbox, SessionState, State, Language, Error) {
+ logger = j.logger("CreateMailbox", session, logger)
+ cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{
+ AccountId: accountId,
+ IfInState: ifInState,
+ Create: map[string]MailboxChange{
+ "c": create,
+ },
+ }, "0"))
+ if err != nil {
+ return Mailbox{}, "", "", "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Mailbox, State, Error) {
+ var setResp MailboxSetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxSet, "0", &setResp)
+ if err != nil {
+ return Mailbox{}, "", err
+ }
+ setErr, notok := setResp.NotCreated["c"]
+ if notok {
+ logger.Error().Msgf("%T.NotCreated returned an error %v", setResp, setErr)
+ return Mailbox{}, "", setErrorError(setErr, MailboxType)
+ }
+ if mailbox, ok := setResp.Created["c"]; ok {
+ return mailbox, setResp.NewState, nil
+ } else {
+ return Mailbox{}, "", simpleError(fmt.Errorf("failed to find created %T in response", Mailbox{}), JmapErrorMissingCreatedObject)
+ }
+ })
+}
+
+func (j *Client) DeleteMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, mailboxIds []string) ([]string, SessionState, State, Language, Error) {
+ logger = j.logger("DeleteMailbox", session, logger)
+ cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{
+ AccountId: accountId,
+ IfInState: ifInState,
+ Destroy: mailboxIds,
+ }, "0"))
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]string, State, Error) {
+ var setResp MailboxSetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandMailboxSet, "0", &setResp)
+ if err != nil {
+ return nil, "", err
+ }
+ setErr, notok := setResp.NotUpdated["u"]
+ if notok {
+ logger.Error().Msgf("%T.NotUpdated returned an error %v", setResp, setErr)
+ return nil, "", setErrorError(setErr, MailboxType)
+ }
+ return setResp.Destroyed, setResp.NewState, nil
})
}
diff --git a/pkg/jmap/jmap_api_quota.go b/pkg/jmap/jmap_api_quota.go
index 3c8f0f6e22..14ba53dc83 100644
--- a/pkg/jmap/jmap_api_quota.go
+++ b/pkg/jmap/jmap_api_quota.go
@@ -7,7 +7,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/structs"
)
-func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]QuotaGetResponse, SessionState, Language, Error) {
+func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]QuotaGetResponse, SessionState, State, Language, Error) {
logger = j.logger("GetQuotas", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
@@ -18,18 +18,18 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
- return nil, "", "", err
+ return nil, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]QuotaGetResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]QuotaGetResponse, State, Error) {
result := map[string]QuotaGetResponse{}
for _, accountId := range uniqueAccountIds {
var response QuotaGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandQuotaGet, mcid(accountId, "0"), &response)
if err != nil {
- return nil, err
+ return nil, "", err
}
result[accountId] = response
}
- return result, nil
+ return result, squashStateFunc(result, func(q QuotaGetResponse) State { return q.State }), nil
})
}
diff --git a/pkg/jmap/jmap_api_vacation.go b/pkg/jmap/jmap_api_vacation.go
index c2221df7ad..ef553e5de8 100644
--- a/pkg/jmap/jmap_api_vacation.go
+++ b/pkg/jmap/jmap_api_vacation.go
@@ -13,19 +13,19 @@ const (
)
// https://jmap.io/spec-mail.html#vacationresponseget
-func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseGetResponse, SessionState, Language, Error) {
+func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseGetResponse, SessionState, State, 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, acceptLanguage, func(body *Response) (VacationResponseGetResponse, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponseGetResponse, State, Error) {
var response VacationResponseGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseGet, "0", &response)
if err != nil {
- return VacationResponseGetResponse{}, err
+ return VacationResponseGetResponse{}, "", err
}
- return response, nil
+ return response, response.State, nil
})
}
@@ -53,12 +53,7 @@ type VacationResponsePayload struct {
HtmlBody string `json:"htmlBody,omitempty"`
}
-type VacationResponseChange struct {
- VacationResponse VacationResponse `json:"vacationResponse"`
- ResponseState State `json:"state"`
-}
-
-func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseChange, SessionState, Language, Error) {
+func (j *Client) SetVacationResponse(accountId string, vacation VacationResponsePayload, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponse, SessionState, State, Language, Error) {
logger = j.logger("SetVacationResponse", session, logger)
cmd, err := j.request(session, logger,
@@ -80,37 +75,34 @@ func (j *Client) SetVacationResponse(accountId string, vacation VacationResponse
invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "1"),
)
if err != nil {
- return VacationResponseChange{}, "", "", err
+ return VacationResponse{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponseChange, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponse, State, Error) {
var setResponse VacationResponseSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseSet, "0", &setResponse)
if err != nil {
- return VacationResponseChange{}, err
+ return VacationResponse{}, "", err
}
setErr, notok := setResponse.NotCreated[vacationResponseId]
if notok {
// this means that the VacationResponse was not updated
logger.Error().Msgf("%T.NotCreated contains an error: %v", setResponse, setErr)
- return VacationResponseChange{}, setErrorError(setErr, VacationResponseType)
+ return VacationResponse{}, "", setErrorError(setErr, VacationResponseType)
}
var getResponse VacationResponseGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseGet, "1", &getResponse)
if err != nil {
- return VacationResponseChange{}, err
+ return VacationResponse{}, "", err
}
if len(getResponse.List) != 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(VacationResponseType), string(CommandVacationResponseGet))
logger.Error().Msg(berr.Error())
- return VacationResponseChange{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
+ return VacationResponse{}, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload)
}
- return VacationResponseChange{
- VacationResponse: getResponse.List[0],
- ResponseState: setResponse.NewState,
- }, nil
+ return getResponse.List[0], setResponse.NewState, nil
})
}
diff --git a/pkg/jmap/jmap_error.go b/pkg/jmap/jmap_error.go
index e81b9578af..d700a5f25a 100644
--- a/pkg/jmap/jmap_error.go
+++ b/pkg/jmap/jmap_error.go
@@ -36,6 +36,7 @@ const (
JmapErrorWssFailedToSendWebSocketPushDisable
JmapErrorWssFailedToClose
JmapErrorWssFailedToRetrieveSession
+ JmapErrorMissingCreatedObject
)
var (
diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go
index 9f1d1d7410..bc412487f8 100644
--- a/pkg/jmap/jmap_integration_test.go
+++ b/pkg/jmap/jmap_integration_test.go
@@ -689,7 +689,7 @@ func TestEmails(t *testing.T) {
var inboxFolder string
var inboxId string
{
- respByAccountId, sessionState, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
+ respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
require.NoError(err)
require.Equal(s.session.State, sessionState)
require.Len(respByAccountId, 1)
@@ -698,7 +698,7 @@ func TestEmails(t *testing.T) {
mailboxesNameByRole := map[string]string{}
mailboxesUnreadByRole := map[string]int{}
- for _, m := range resp.Mailboxes {
+ for _, m := range resp {
if m.Role != "" {
mailboxesNameByRole[m.Role] = m.Name
mailboxesUnreadByRole[m.Role] = m.UnreadEmails
@@ -708,7 +708,7 @@ func TestEmails(t *testing.T) {
require.Contains(mailboxesUnreadByRole, "inbox")
require.Zero(mailboxesUnreadByRole["inbox"])
- inboxId = mailboxId("inbox", resp.Mailboxes)
+ inboxId = mailboxId("inbox", resp)
require.NotEmpty(inboxId)
inboxFolder = mailboxesNameByRole["inbox"]
require.NotEmpty(inboxFolder)
@@ -724,23 +724,23 @@ func TestEmails(t *testing.T) {
{
{
- resp, sessionState, _, err := s.client.GetAllIdentities(accountId, s.session, s.ctx, s.logger, "")
+ resp, sessionState, _, _, err := s.client.GetAllIdentities(accountId, s.session, s.ctx, s.logger, "")
require.NoError(err)
require.Equal(s.session.State, sessionState)
- require.Len(resp.Identities, 1)
- require.Equal(s.userEmail, resp.Identities[0].Email)
- require.Equal(s.userPersonName, resp.Identities[0].Name)
+ require.Len(resp, 1)
+ require.Equal(s.userEmail, resp[0].Email)
+ require.Equal(s.userPersonName, resp[0].Name)
}
{
- respByAccountId, sessionState, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
+ respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
require.NoError(err)
require.Equal(s.session.State, sessionState)
require.Len(respByAccountId, 1)
require.Contains(respByAccountId, accountId)
resp := respByAccountId[accountId]
mailboxesUnreadByRole := map[string]int{}
- for _, m := range resp.Mailboxes {
+ for _, m := range resp {
if m.Role != "" {
mailboxesUnreadByRole[m.Role] = m.UnreadEmails
}
@@ -749,7 +749,7 @@ func TestEmails(t *testing.T) {
}
{
- resp, sessionState, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, true, false, 0, true)
+ resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, true, false, 0, true)
require.NoError(err)
require.Equal(s.session.State, sessionState)
@@ -766,7 +766,7 @@ func TestEmails(t *testing.T) {
}
{
- resp, sessionState, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, false, false, 0, true)
+ resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, false, false, 0, true)
require.NoError(err)
require.Equal(s.session.State, sessionState)
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 34660f62fb..8a6ae7599b 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -1201,7 +1201,7 @@ type Mailbox struct {
// (that has the same parent) in any Mailbox listing in the client’s UI.
// Mailboxes with equal order SHOULD be sorted in alphabetical order by name.
// The sorting should take into account locale-specific character order convention.
- SortOrder int `json:"sortOrder,omitzero"`
+ SortOrder *int `json:"sortOrder,omitempty"`
// The number of Emails in this Mailbox.
TotalEmails int `json:"totalEmails"`
@@ -1220,7 +1220,7 @@ type Mailbox struct {
// These are backwards compatible with IMAP ACLs, as defined in [RFC4314].
//
// [RFC4314]: https://www.rfc-editor.org/rfc/rfc4314.html
- MyRights MailboxRights `json:"myRights,omitempty"`
+ MyRights *MailboxRights `json:"myRights,omitempty"`
// Has the user indicated they wish to see this Mailbox in their client?
//
@@ -1241,7 +1241,96 @@ type Mailbox struct {
// This property corresponds to IMAP [RFC3501] Mailbox subscriptions.
//
// [RFC3501]: https://www.rfc-editor.org/rfc/rfc3501.html
- IsSubscribed bool `json:"isSubscribed"`
+ IsSubscribed *bool `json:"isSubscribed,omitempty"`
+}
+
+type MailboxChange struct {
+ // User-visible name for the Mailbox, e.g., “Inbox”.
+ //
+ // This MUST be a Net-Unicode string [@!RFC5198] of at least 1 character in length, subject to the maximum size
+ // given in the capability object.
+ //
+ // There MUST NOT be two sibling Mailboxes with both the same parent and the same name.
+ //
+ // Servers MAY reject names that violate server policy (e.g., names containing a slash (/) or control characters).
+ Name string `json:"name,omitempty"`
+
+ // The Mailbox id for the parent of this Mailbox, or null if this Mailbox is at the top level.
+ //
+ // Mailboxes form acyclic graphs (forests) directed by the child-to-parent relationship. There MUST NOT be a loop.
+ ParentId string `json:"parentId,omitempty"`
+
+ // Identifies Mailboxes that have a particular common purpose (e.g., the “inbox”), regardless of the name property
+ // (which may be localised).
+ //
+ // This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE extension [RFC6154]).
+ // However, unlike in IMAP, a Mailbox MUST only have a single role, and there MUST NOT be two Mailboxes in the same
+ // account with the same role.
+ //
+ // Servers providing IMAP access to the same data are encouraged to enforce these extra restrictions in IMAP as well.
+ // Otherwise, modifying the IMAP attributes to ensure compliance when exposing the data over JMAP is implementation dependent.
+ //
+ // The value MUST be one of the Mailbox attribute names listed in the IANA IMAP Mailbox Name Attributes registry,
+ // as established in [RFC8457], converted to lowercase. New roles may be established here in the future.
+ //
+ // An account is not required to have Mailboxes with any particular roles.
+ //
+ // [RFC6154]: https://www.rfc-editor.org/rfc/rfc6154.html
+ // [RFC8457]: https://www.rfc-editor.org/rfc/rfc8457.html
+ Role string `json:"role,omitempty"`
+
+ // Defines the sort order of Mailboxes when presented in the client’s UI, so it is consistent between devices.
+ //
+ // Default value: 0
+ //
+ // The number MUST be an integer in the range 0 <= sortOrder < 2^31.
+ //
+ // A Mailbox with a lower order should be displayed before a Mailbox with a higher order
+ // (that has the same parent) in any Mailbox listing in the client’s UI.
+ // Mailboxes with equal order SHOULD be sorted in alphabetical order by name.
+ // The sorting should take into account locale-specific character order convention.
+ SortOrder *int `json:"sortOrder,omitempty"`
+
+ // Has the user indicated they wish to see this Mailbox in their client?
+ //
+ // This SHOULD default to false for Mailboxes in shared accounts the user has access to and true
+ // for any new Mailboxes created by the user themself.
+ //
+ // This MUST be stored separately per user where multiple users have access to a shared Mailbox.
+ //
+ // A user may have permission to access a large number of shared accounts, or a shared account with a very
+ // large set of Mailboxes, but only be interested in the contents of a few of these.
+ //
+ // Clients may choose to only display Mailboxes where the isSubscribed property is set to true, and offer
+ // a separate UI to allow the user to see and subscribe/unsubscribe from the full set of Mailboxes.
+ //
+ // However, clients MAY choose to ignore this property, either entirely for ease of implementation or just
+ // for an account where isPersonal is true (indicating it is the user’s own rather than a shared account).
+ //
+ // This property corresponds to IMAP [RFC3501] Mailbox subscriptions.
+ //
+ // [RFC3501]: https://www.rfc-editor.org/rfc/rfc3501.html
+ IsSubscribed *bool `json:"isSubscribed,omitempty"`
+}
+
+func (m MailboxChange) AsPatch() PatchObject {
+ p := PatchObject{}
+ if m.Name != "" {
+ p["name"] = m.Name
+ }
+ if m.ParentId != "" {
+ p["parentId"] = m.ParentId
+ }
+ if m.Role != "" {
+ p["role"] = m.Role
+ }
+ if m.SortOrder != nil {
+ p["sortOrder"] = m.SortOrder
+ }
+ if m.IsSubscribed != nil {
+ p["isSubscribed"] = m.IsSubscribed
+ }
+ return p
}
type MailboxGetCommand struct {
@@ -1254,6 +1343,26 @@ type MailboxGetRefCommand struct {
IdsRef *ResultReference `json:"#ids,omitempty"`
}
+type MailboxSetCommand struct {
+ AccountId string `json:"accountId"`
+ IfInState string `json:"ifInState,omitempty"`
+ Create map[string]MailboxChange `json:"create,omitempty"`
+ Update map[string]PatchObject `json:"update,omitempty"`
+ Destroy []string `json:"destroy,omitempty"`
+}
+
+type MailboxSetResponse struct {
+ AccountId string `json:"accountId"`
+ OldState State `json:"oldState,omitempty"`
+ NewState State `json:"newState,omitempty"`
+ Created map[string]Mailbox `json:"created,omitempty"`
+ Updated map[string]Mailbox `json:"updated,omitempty"`
+ Destroyed []string `json:"destroyed,omitempty"`
+ NotCreated map[string]SetError `json:"notCreated,omitempty"`
+ NotUpdated map[string]SetError `json:"notUpdated,omitempty"`
+ NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
+}
+
type MailboxChangesCommand struct {
// The id of the account to use.
AccountId string `json:"accountId"`
@@ -5251,6 +5360,7 @@ const (
CommandEmailSubmissionSet Command = "EmailSubmission/set"
CommandThreadGet Command = "Thread/get"
CommandMailboxGet Command = "Mailbox/get"
+ CommandMailboxSet Command = "Mailbox/set"
CommandMailboxQuery Command = "Mailbox/query"
CommandMailboxChanges Command = "Mailbox/changes"
CommandIdentityGet Command = "Identity/get"
@@ -5272,6 +5382,7 @@ var CommandResponseTypeMap = map[Command]func() any{
CommandBlobUpload: func() any { return BlobUploadResponse{} },
CommandMailboxQuery: func() any { return MailboxQueryResponse{} },
CommandMailboxGet: func() any { return MailboxGetResponse{} },
+ CommandMailboxSet: func() any { return MailboxSetResponse{} },
CommandMailboxChanges: func() any { return MailboxChangesResponse{} },
CommandEmailQuery: func() any { return EmailQueryResponse{} },
CommandEmailChanges: func() any { return EmailChangesResponse{} },
diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go
index 43056abcab..47b0608c0d 100644
--- a/pkg/jmap/jmap_test.go
+++ b/pkg/jmap/jmap_test.go
@@ -231,15 +231,15 @@ 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")
folders := foldersByAccountId["a"]
- require.Len(folders.Mailboxes, 5)
+ require.Len(folders, 5)
require.NotEmpty(sessionState)
- emails, sessionState, _, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "", "Inbox", 0, 0, false, true, 0, true)
+ emails, sessionState, _, _, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "", "Inbox", 0, 0, false, true, 0, true)
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 cb823cd7a9..a607767c49 100644
--- a/pkg/jmap/jmap_tools.go
+++ b/pkg/jmap/jmap_tools.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"reflect"
+ "slices"
"strings"
"sync"
"time"
@@ -57,12 +58,12 @@ func command[T any](api ApiClient,
sessionOutdatedHandler func(session *Session, newState SessionState),
request Request,
acceptLanguage string,
- mapper func(body *Response) (T, Error)) (T, SessionState, Language, Error) {
+ mapper func(body *Response) (T, State, Error)) (T, SessionState, State, Language, Error) {
responseBody, language, jmapErr := api.Command(ctx, logger, session, request, acceptLanguage)
if jmapErr != nil {
var zero T
- return zero, "", language, 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, "", language, 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, language, 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, language, SimpleError{code: code, err: err}
+ return zero, response.SessionState, "", language, SimpleError{code: code, err: err}
}
}
}
- result, jerr := mapper(&response)
+ result, state, jerr := mapper(&response)
sessionState := response.SessionState
- return result, sessionState, language, jerr
+ return result, sessionState, state, language, jerr
}
func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
@@ -256,3 +257,74 @@ func (i *Invocation) UnmarshalJSON(bs []byte) error {
i.Parameters = params
return nil
}
+
+func squashState(all map[string]State) State {
+ return squashStateFunc(all, func(s State) State { return s })
+}
+
+func squashStateFunc[V any](all map[string]V, mapper func(V) State) State {
+ n := len(all)
+ if n == 0 {
+ return State("")
+ }
+ if n == 1 {
+ for _, v := range all {
+ return mapper(v)
+ }
+ }
+
+ parts := make([]string, n)
+ sortedKeys := make([]string, n)
+ i := 0
+ for k := range all {
+ sortedKeys[i] = k
+ i++
+ }
+ slices.Sort(sortedKeys)
+ for i, k := range sortedKeys {
+ if v, ok := all[k]; ok {
+ parts[i] = k + ":" + string(mapper(v))
+ } else {
+ parts[i] = k + ":"
+ }
+ }
+ return State(strings.Join(parts, ","))
+}
+
+func squashStateMaps(first map[string]State, second map[string]State) State {
+ return squashStateFunc(mapPairs(first, second), func(p pair[State, State]) State {
+ if p.left != nil {
+ if p.right != nil {
+ return *p.left + ";" + *p.right
+ } else {
+ return *p.left + ";"
+ }
+ } else if p.right != nil {
+ return ";" + *p.right
+ } else {
+ return ";"
+ }
+ })
+}
+
+type pair[L any, R any] struct {
+ left *L
+ right *R
+}
+
+func mapPairs[K comparable, L, R any](left map[K]L, right map[K]R) map[K]pair[L, R] {
+ result := map[K]pair[L, R]{}
+ for k, l := range left {
+ if r, ok := right[k]; ok {
+ result[k] = pair[L, R]{left: &l, right: &r}
+ } else {
+ result[k] = pair[L, R]{left: &l, right: nil}
+ }
+ }
+ for k, r := range right {
+ if _, ok := left[k]; !ok {
+ result[k] = pair[L, R]{left: nil, right: &r}
+ }
+ }
+ return result
+}
diff --git a/pkg/jmap/jmap_tools_test.go b/pkg/jmap/jmap_tools_test.go
index 621331e541..2d20576790 100644
--- a/pkg/jmap/jmap_tools_test.go
+++ b/pkg/jmap/jmap_tools_test.go
@@ -50,7 +50,7 @@ func TestDeserializeMailboxGetResponse(t *testing.T) {
require.Equal(expected.unread, folder.UnreadThreads)
require.Empty(folder.ParentId)
require.Zero(folder.SortOrder)
- require.True(folder.IsSubscribed)
+ require.Nil(folder.IsSubscribed)
require.True(folder.MyRights.MayReadItems)
require.True(folder.MyRights.MayAddItems)
diff --git a/services/groupware/pkg/groupware/groupware_api_blob.go b/services/groupware/pkg/groupware/groupware_api_blob.go
index 73ae4c198c..4cf220fe4f 100644
--- a/services/groupware/pkg/groupware/groupware_api_blob.go
+++ b/services/groupware/pkg/groupware/groupware_api_blob.go
@@ -28,20 +28,15 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
- res, sessionState, lang, jerr := g.jmap.GetBlobMetadata(accountId, req.session, req.ctx, logger, req.language(), blobId)
+ res, sessionState, state, lang, jerr := g.jmap.GetBlobMetadata(accountId, req.session, req.ctx, logger, req.language(), blobId)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- blob := res.Blob
+ blob := res
if blob == nil {
return notFoundResponse(sessionState)
}
- digest := blob.Digest()
- if digest != "" {
- return etagResponse(res, sessionState, jmap.State(digest), lang)
- } else {
- return response(res, sessionState, lang)
- }
+ return etagResponse(res, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go
index ee8f5981a6..a1d698d623 100644
--- a/services/groupware/pkg/groupware/groupware_api_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_api_calendars.go
@@ -121,10 +121,10 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
l := req.logger.With().Array(UriParamBlobId, log.SafeStringArray(blobIds))
logger := log.From(l)
- resp, sessionState, lang, jerr := g.jmap.ParseICalendarBlob(accountId, req.session, req.ctx, logger, req.language(), blobIds)
+ resp, sessionState, state, lang, jerr := g.jmap.ParseICalendarBlob(accountId, req.session, req.ctx, logger, req.language(), blobIds)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return response(resp, sessionState, lang)
+ return etagResponse(resp, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_contacts.go b/services/groupware/pkg/groupware/groupware_api_contacts.go
index acca85eacc..e93327c8b1 100644
--- a/services/groupware/pkg/groupware/groupware_api_contacts.go
+++ b/services/groupware/pkg/groupware/groupware_api_contacts.go
@@ -32,12 +32,12 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
return resp
}
- addressbooks, sessionState, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, req.logger, req.language(), nil)
+ addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, req.logger, req.language(), nil)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return response(addressbooks, sessionState, lang)
+ return etagResponse(addressbooks, sessionState, state, lang)
})
}
@@ -72,7 +72,7 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
logger := log.From(l)
- addressbooks, sessionState, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, logger, req.language(), []string{addressBookId})
+ addressbooks, sessionState, state, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, logger, req.language(), []string{addressBookId})
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -80,7 +80,7 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
if len(addressbooks.NotFound) > 0 {
return notFoundResponse(sessionState)
} else {
- return response(addressbooks, sessionState, lang)
+ return etagResponse(addressbooks, sessionState, state, lang)
}
})
}
@@ -135,13 +135,13 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
sortBy := []jmap.ContactCardComparator{{Property: jscontact.ContactCardPropertyUpdated, IsAscending: false}}
logger := log.From(l)
- contactsByAccountId, sessionState, lang, jerr := g.jmap.QueryContactCards([]string{accountId}, req.session, req.ctx, logger, req.language(), filter, sortBy, offset, limit)
+ contactsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryContactCards([]string{accountId}, req.session, req.ctx, logger, req.language(), filter, sortBy, offset, limit)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
if contacts, ok := contactsByAccountId[accountId]; ok {
- return response(contacts, req.session.State, lang)
+ return etagResponse(contacts, sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
@@ -167,11 +167,11 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(l)
- created, sessionState, lang, jerr := g.jmap.CreateContactCard(accountId, req.session, req.ctx, logger, req.language(), create)
+ created, sessionState, state, lang, jerr := g.jmap.CreateContactCard(accountId, req.session, req.ctx, logger, req.language(), create)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return etagResponse(created.ContactCard, sessionState, created.State, lang)
+ return etagResponse(created, sessionState, state, lang)
})
}
@@ -188,12 +188,12 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
logger := log.From(l)
- deleted, sessionState, _, jerr := g.jmap.DeleteContactCard(accountId, []string{contactId}, req.session, req.ctx, logger, req.language())
+ deleted, sessionState, state, _, jerr := g.jmap.DeleteContactCard(accountId, []string{contactId}, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- for _, e := range deleted.NotDestroyed {
+ for _, e := range deleted {
desc := e.Description
if desc != "" {
return errorResponseWithSessionState(apiError(
@@ -208,6 +208,6 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
), sessionState)
}
}
- return noContentResponseWithEtag(sessionState, deleted.State)
+ return noContentResponseWithEtag(sessionState, state)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go
index 1ffca3926f..bd2f91fad1 100644
--- a/services/groupware/pkg/groupware/groupware_api_emails.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -79,12 +79,12 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
logger := log.From(req.logger.With().Str(HeaderSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
- changes, sessionState, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
+ changes, sessionState, state, 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(changes, sessionState, changes.State, lang)
+ return etagResponse(changes, sessionState, state, lang)
})
} else {
g.respond(w, r, func(req Request) Response {
@@ -116,7 +116,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
logger := log.From(l)
- emails, sessionState, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, false, true, g.maxBodyValueBytes, true)
+ emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, false, true, g.maxBodyValueBytes, true)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -133,7 +133,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
Offset: emails.Offset,
}
- return etagResponse(safe, sessionState, emails.State, lang)
+ return etagResponse(safe, sessionState, state, lang)
})
}
}
@@ -164,7 +164,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)).Str("id", log.SafeString(id)).Str("accept", log.SafeString(accept)))
- blobId, _, _, jerr := g.jmap.GetEmailBlobId(accountId, req.session, req.ctx, logger, req.language(), id)
+ blobId, _, _, _, jerr := g.jmap.GetEmailBlobId(accountId, req.session, req.ctx, logger, req.language(), id)
if jerr != nil {
return req.apiErrorFromJmap(req.observeJmapError(jerr))
}
@@ -203,34 +203,34 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
if len(ids) == 1 {
logger := log.From(l.Str("id", log.SafeString(id)))
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, true)
+ emails, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, true)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(emails.Emails) < 1 {
+ if len(emails) < 1 {
return notFoundResponse(sessionState)
} else {
- sanitized, err := req.sanitizeEmail(emails.Emails[0])
+ sanitized, err := req.sanitizeEmail(emails[0])
if err != nil {
return errorResponseWithSessionState(err, sessionState)
}
- return etagResponse(sanitized, sessionState, emails.State, lang)
+ return etagResponse(sanitized, sessionState, state, lang)
}
} else {
logger := log.From(l.Array("ids", log.SafeStringArray(ids)))
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, false)
+ emails, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), ids, true, g.maxBodyValueBytes, markAsSeen, false)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(emails.Emails) < 1 {
+ if len(emails) < 1 {
return notFoundResponse(sessionState)
} else {
- sanitized, err := req.sanitizeEmails(emails.Emails)
+ sanitized, err := req.sanitizeEmails(emails)
if err != nil {
return errorResponseWithSessionState(err, sessionState)
}
- return etagResponse(sanitized, sessionState, emails.State, lang)
+ return etagResponse(sanitized, sessionState, state, lang)
}
}
})
@@ -269,18 +269,18 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
}
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
logger := log.From(l)
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
+ emails, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(emails.Emails) < 1 {
+ if len(emails) < 1 {
return notFoundResponse(sessionState)
}
- email, err := req.sanitizeEmail(emails.Emails[0])
+ email, err := req.sanitizeEmail(emails[0])
if err != nil {
return errorResponseWithSessionState(err, sessionState)
}
- return etagResponse(email.Attachments, sessionState, emails.State, lang)
+ return etagResponse(email.Attachments, sessionState, state, lang)
})
} else {
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
@@ -297,15 +297,15 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
l = contextAppender(l)
logger := log.From(l)
- emails, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
+ emails, _, _, lang, jerr := g.jmap.GetEmails(mailAccountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
if jerr != nil {
return req.apiErrorFromJmap(req.observeJmapError(jerr))
}
- if len(emails.Emails) < 1 {
+ if len(emails) < 1 {
return nil
}
- email, err := req.sanitizeEmail(emails.Emails[0])
+ email, err := req.sanitizeEmail(emails[0])
if err != nil {
return err
}
@@ -381,12 +381,12 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since
logger := log.From(l)
- changes, sessionState, lang, jerr := g.jmap.GetEmailsSince(accountId, req.session, req.ctx, logger, req.language(), since, true, g.maxBodyValueBytes, maxChanges)
+ changes, sessionState, state, 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(changes, sessionState, changes.State, lang)
+ return etagResponse(changes, sessionState, state, lang)
})
}
@@ -609,7 +609,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) {
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- resultsByAccount, sessionState, lang, jerr := g.jmap.QueryEmailsWithSnippets([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
+ resultsByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -645,7 +645,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) {
Total: results.Total,
Limit: results.Limit,
QueryState: results.QueryState,
- }, sessionState, results.QueryState, lang)
+ }, sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
@@ -656,7 +656,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) {
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmailSnippets([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit)
+ resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSnippets([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -667,7 +667,7 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) {
Total: results.Total,
Limit: results.Limit,
QueryState: results.QueryState,
- }, sessionState, results.QueryState, lang)
+ }, sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
@@ -722,7 +722,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
}
if makesSnippets {
- resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmailsWithSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
+ resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailsWithSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -771,7 +771,6 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
}
slices.SortFunc(flattened, func(a, b EmailWithSnippets) int { return a.ReceivedAt.Compare(b.ReceivedAt) })
- squashedQueryState := squashQueryState(resultsByAccountId, func(e jmap.EmailQueryWithSnippetsResult) jmap.State { return e.QueryState })
// TODO offset and limit over the aggregated results by account
@@ -779,10 +778,10 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
Results: flattened,
Total: totalOverAllAccounts,
Limit: limit,
- QueryState: squashedQueryState,
- }, sessionState, squashedQueryState, lang)
+ QueryState: state,
+ }, sessionState, state, lang)
} else {
- resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmails(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
+ resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmails(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -810,7 +809,6 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
}
slices.SortFunc(flattened, func(a, b jmap.Email) int { return a.ReceivedAt.Compare(b.ReceivedAt) })
- squashedQueryState := squashQueryState(resultsByAccountId, func(e jmap.EmailQueryResult) jmap.State { return e.QueryState })
// TODO offset and limit over the aggregated results by account
@@ -818,12 +816,12 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
Results: flattened,
Total: totalOverAllAccounts,
Limit: limit,
- QueryState: squashedQueryState,
- }, sessionState, squashedQueryState, lang)
+ QueryState: state,
+ }, sessionState, state, lang)
}
} else {
if makesSnippets {
- resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit)
+ resultsByAccountId, sessionState, state, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -852,14 +850,12 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
// TODO offset and limit over the aggregated results by account
- squashedQueryState := squashQueryState(resultsByAccountId, func(e jmap.EmailSnippetQueryResult) jmap.State { return e.QueryState })
-
return etagResponse(EmailSearchSnippetsResults{
Results: flattened,
Total: totalOverAllAccounts,
Limit: limit,
- QueryState: squashedQueryState,
- }, sessionState, squashedQueryState, lang)
+ QueryState: state,
+ }, sessionState, state, lang)
} else {
// TODO implement search without email bodies (only retrieve a few chosen properties?) + without snippets
return notImplementesResponse()
@@ -908,12 +904,12 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
BodyValues: body.BodyValues,
}
- created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, "", req.session, req.ctx, logger, req.language())
+ created, sessionState, state, 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, lang)
+ return etagResponse(created, sessionState, state, lang)
})
}
@@ -947,12 +943,12 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
BodyValues: body.BodyValues,
}
- created, sessionState, lang, jerr := g.jmap.CreateEmail(accountId, create, replaceId, req.session, req.ctx, logger, req.language())
+ created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, create, replaceId, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return response(created.Email, sessionState, lang)
+ return etagResponse(created, sessionState, state, lang)
})
}
@@ -989,22 +985,22 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
emailId: body,
}
- result, sessionState, lang, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger, req.language())
+ result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, updates, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if result.Updated == nil {
+ if result == nil {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response",
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")))
}
- updatedEmail, ok := result.Updated[emailId]
+ updatedEmail, ok := result[emailId]
if !ok {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID",
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
}
- return response(updatedEmail, sessionState, lang)
+ return etagResponse(updatedEmail, sessionState, state, lang)
})
}
@@ -1053,22 +1049,22 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
emailId: patch,
}
- result, sessionState, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
+ result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if result.Updated == nil {
+ if result == nil {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response",
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")))
}
- updatedEmail, ok := result.Updated[emailId]
+ updatedEmail, ok := result[emailId]
if !ok {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID",
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
}
- return response(updatedEmail, sessionState, lang)
+ return etagResponse(updatedEmail, sessionState, state, lang)
})
}
@@ -1114,25 +1110,25 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
emailId: patch,
}
- result, sessionState, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
+ result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if result.Updated == nil {
+ if result == nil {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response",
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")))
}
- updatedEmail, ok := result.Updated[emailId]
+ updatedEmail, ok := result[emailId]
if !ok {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID",
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
}
if updatedEmail == nil {
- return noContentResponseWithEtag(sessionState, result.State)
+ return noContentResponseWithEtag(sessionState, state)
} else {
- return response(updatedEmail, sessionState, lang)
+ return etagResponse(updatedEmail, sessionState, state, lang)
}
})
}
@@ -1179,25 +1175,25 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
emailId: patch,
}
- result, sessionState, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
+ result, sessionState, state, lang, jerr := g.jmap.UpdateEmails(accountId, patches, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if result.Updated == nil {
+ if result == nil {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response",
"An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")))
}
- updatedEmail, ok := result.Updated[emailId]
+ updatedEmail, ok := result[emailId]
if !ok {
return errorResponse(apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID",
"An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")))
}
if updatedEmail == nil {
- return noContentResponseWithEtag(sessionState, result.State)
+ return noContentResponseWithEtag(sessionState, state)
} else {
- return response(updatedEmail, sessionState, lang)
+ return etagResponse(updatedEmail, sessionState, state, lang)
}
})
}
@@ -1226,12 +1222,12 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
logger := log.From(l)
- resp, sessionState, _, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger, req.language())
+ resp, sessionState, state, _, jerr := g.jmap.DeleteEmails(accountId, []string{emailId}, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- for _, e := range resp.NotDestroyed {
+ for _, e := range resp {
desc := e.Description
if desc != "" {
return errorResponseWithSessionState(apiError(
@@ -1246,7 +1242,7 @@ func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
), sessionState)
}
}
- return noContentResponseWithEtag(sessionState, resp.State)
+ return noContentResponseWithEtag(sessionState, state)
})
}
@@ -1289,14 +1285,14 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
logger := log.From(l)
- resp, sessionState, _, jerr := g.jmap.DeleteEmails(accountId, emailIds, req.session, req.ctx, logger, req.language())
+ resp, sessionState, state, _, jerr := g.jmap.DeleteEmails(accountId, emailIds, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(resp.NotDestroyed) > 0 {
- meta := make(map[string]any, len(resp.NotDestroyed))
- for emailId, e := range resp.NotDestroyed {
+ if len(resp) > 0 {
+ meta := make(map[string]any, len(resp))
+ for emailId, e := range resp {
meta[emailId] = e.Description
}
return errorResponseWithSessionState(apiError(
@@ -1305,7 +1301,7 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
withMeta(meta),
), sessionState)
}
- return noContentResponseWithEtag(sessionState, resp.State)
+ return noContentResponseWithEtag(sessionState, state)
})
}
@@ -1391,13 +1387,13 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
reqId := req.GetRequestId()
getEmailsBefore := time.Now()
- emails, sessionState, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes, false, false)
+ emails, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, true, g.maxBodyValueBytes, false, false)
getEmailsDuration := time.Since(getEmailsBefore)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(emails.Emails) < 1 {
+ if len(emails) < 1 {
req.observe(g.metrics.EmailByIdDuration.WithLabelValues(req.session.JmapEndpoint, metrics.Values.Result.NotFound), getEmailsDuration.Seconds())
logger.Trace().Msg("failed to find any emails matching id") // the id is already in the log field
return notFoundResponse(sessionState)
@@ -1405,7 +1401,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
req.observe(g.metrics.EmailByIdDuration.WithLabelValues(req.session.JmapEndpoint, metrics.Values.Result.Found), getEmailsDuration.Seconds())
}
- email := emails.Emails[0]
+ email := emails[0]
beacon := email.ReceivedAt // TODO configurable: either relative to when the email was received, or relative to now
//beacon := time.Now()
@@ -1416,7 +1412,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
g.job(logger, RelationTypeSameSender, func(jobId uint64, l *log.Logger) {
before := time.Now()
- resultsByAccountId, _, lang, jerr := g.jmap.QueryEmails([]string{accountId}, filter, req.session, bgctx, l, req.language(), 0, limit, false, g.maxBodyValueBytes)
+ resultsByAccountId, _, _, lang, jerr := g.jmap.QueryEmails([]string{accountId}, filter, req.session, bgctx, l, req.language(), 0, limit, false, g.maxBodyValueBytes)
if results, ok := resultsByAccountId[accountId]; ok {
duration := time.Since(before)
if jerr != nil {
@@ -1437,7 +1433,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
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, req.language(), 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)
@@ -1461,7 +1457,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
return etagResponse(AboutEmailResponse{
Email: sanitized,
RequestId: reqId,
- }, sessionState, emails.State, lang)
+ }, sessionState, state, lang)
})
}
@@ -1777,7 +1773,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
logger := log.From(l)
- emailsSummariesByAccount, sessionState, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit, true)
+ emailsSummariesByAccount, sessionState, state, lang, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, req.language(), filter, limit, true)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -1803,12 +1799,12 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
summaries[i] = summarizeEmail(all[i].accountId, all[i].email)
}
- return response(EmailSummaries{
+ return etagResponse(EmailSummaries{
Emails: summaries,
Total: total,
Limit: limit,
Offset: offset,
- }, sessionState, lang)
+ }, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go
index e2ebf5516a..f4740e8772 100644
--- a/services/groupware/pkg/groupware/groupware_api_identity.go
+++ b/services/groupware/pkg/groupware/groupware_api_identity.go
@@ -15,9 +15,7 @@ import (
// swagger:response GetIdentitiesResponse
type SwaggerGetIdentitiesResponse struct {
// in: body
- Body struct {
- *jmap.Identities
- }
+ Body []jmap.Identity
}
// swagger:route GET /groupware/accounts/{account}/identities identity identities
@@ -36,11 +34,11 @@ func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
- res, sessionState, lang, jerr := g.jmap.GetAllIdentities(accountId, req.session, req.ctx, logger, req.language())
+ res, sessionState, state, lang, jerr := g.jmap.GetAllIdentities(accountId, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return etagResponse(res, sessionState, res.State, lang)
+ return etagResponse(res, sessionState, state, lang)
})
}
@@ -52,14 +50,14 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
}
id := chi.URLParam(r, UriParamIdentityId)
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
- res, sessionState, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id})
+ res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id})
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- if len(res.Identities) < 1 {
+ if len(res) < 1 {
return notFoundResponse(sessionState)
}
- return etagResponse(res.Identities[0], sessionState, res.State, lang)
+ return etagResponse(res[0], sessionState, state, lang)
})
}
@@ -77,11 +75,11 @@ func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
- newState, sessionState, _, jerr := g.jmap.CreateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity)
+ created, sessionState, state, lang, jerr := g.jmap.CreateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return noContentResponseWithEtag(sessionState, newState)
+ return etagResponse(created, sessionState, state, lang)
})
}
@@ -99,11 +97,11 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
- newState, sessionState, _, jerr := g.jmap.UpdateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity)
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return noContentResponseWithEtag(sessionState, newState)
+ return etagResponse(updated, sessionState, state, lang)
})
}
@@ -121,14 +119,14 @@ func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
return req.parameterErrorResponse(UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
}
- deletion, sessionState, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
+ deletion, sessionState, state, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- notDeletedIds := structs.Missing(ids, deletion.Destroyed)
+ notDeletedIds := structs.Missing(ids, deletion)
if len(notDeletedIds) == 0 {
- return noContentResponseWithEtag(sessionState, deletion.NewState)
+ return noContentResponseWithEtag(sessionState, state)
} else {
logger.Error().Array("not-deleted", log.SafeStringArray(notDeletedIds)).Msgf("failed to delete %d identities", len(notDeletedIds))
return errorResponseWithSessionState(req.apiError(&ErrorFailedToDeleteSomeIdentities,
diff --git a/services/groupware/pkg/groupware/groupware_api_index.go b/services/groupware/pkg/groupware/groupware_api_index.go
index 58a6b03e2e..e2adea216f 100644
--- a/services/groupware/pkg/groupware/groupware_api_index.go
+++ b/services/groupware/pkg/groupware/groupware_api_index.go
@@ -162,18 +162,18 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
accountIds := structs.Keys(req.session.Accounts)
- boot, sessionState, lang, err := g.jmap.GetBootstrap(accountIds, req.session, req.ctx, req.logger, req.language())
+ boot, sessionState, state, lang, err := g.jmap.GetBootstrap(accountIds, req.session, req.ctx, req.logger, req.language())
if err != nil {
return req.errorResponseFromJmap(err)
}
- return response(IndexResponse{
+ return etagResponse(IndexResponse{
Version: Version,
Capabilities: Capabilities,
Limits: buildIndexLimits(req.session),
Accounts: buildIndexAccounts(req.session, boot),
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
- }, sessionState, lang)
+ }, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_mailbox.go b/services/groupware/pkg/groupware/groupware_api_mailbox.go
index 5e4953959f..e4b0635d67 100644
--- a/services/groupware/pkg/groupware/groupware_api_mailbox.go
+++ b/services/groupware/pkg/groupware/groupware_api_mailbox.go
@@ -43,13 +43,13 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
return errorResponse(err)
}
- mailboxes, sessionState, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId})
+ mailboxes, sessionState, state, 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, lang)
+ return etagResponse(mailboxes.Mailboxes[0], sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
@@ -125,23 +125,23 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
logger := log.From(req.logger.With().Str(logAccountId, accountId))
if hasCriteria {
- mailboxesByAccountId, sessionState, lang, err := g.jmap.SearchMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language(), filter)
+ mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language(), filter)
if err != nil {
return req.errorResponseFromJmap(err)
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
- return etagResponse(sortMailboxSlice(mailboxes.Mailboxes), sessionState, mailboxes.State, lang)
+ return etagResponse(sortMailboxSlice(mailboxes), sessionState, state, lang)
} else {
return notFoundResponse(sessionState)
}
} else {
- mailboxesByAccountId, sessionState, lang, err := g.jmap.GetAllMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language())
+ mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes([]string{accountId}, req.session, req.ctx, logger, req.language())
if err != nil {
return req.errorResponseFromJmap(err)
}
if mailboxes, ok := mailboxesByAccountId[accountId]; ok {
- return etagResponse(sortMailboxSlice(mailboxes.Mailboxes), sessionState, mailboxes.State, lang)
+ return etagResponse(sortMailboxSlice(mailboxes), sessionState, 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, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
+ mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
if err != nil {
return req.errorResponseFromJmap(err)
}
- return response(sortMailboxesMap(mailboxesByAccountId), sessionState, lang)
+ return etagResponse(sortMailboxesMap(mailboxesByAccountId), sessionState, state, lang)
} else {
- mailboxesByAccountId, sessionState, lang, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger, req.language())
+ mailboxesByAccountId, sessionState, state, lang, err := g.jmap.GetAllMailboxes(accountIds, req.session, req.ctx, logger, req.language())
if err != nil {
return req.errorResponseFromJmap(err)
}
- return response(sortMailboxesMap(mailboxesByAccountId), sessionState, lang)
+ return etagResponse(sortMailboxesMap(mailboxesByAccountId), sessionState, state, lang)
}
})
}
@@ -221,11 +221,11 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt
Role: role,
}
- mailboxesByAccountId, sessionState, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
+ mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
if err != nil {
return req.errorResponseFromJmap(err)
}
- return response(sortMailboxesMap(mailboxesByAccountId), sessionState, lang)
+ return etagResponse(sortMailboxesMap(mailboxesByAccountId), sessionState, state, lang)
})
}
@@ -267,12 +267,12 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
logger := log.From(l)
- changes, sessionState, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.maxBodyValueBytes, maxChanges)
+ changes, sessionState, state, 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, lang)
+ return etagResponse(changes, sessionState, state, lang)
})
}
@@ -320,12 +320,12 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
logger := log.From(l)
- changesByAccountId, sessionState, lang, jerr := g.jmap.GetMailboxChangesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language(), sinceStateMap, true, g.maxBodyValueBytes, maxChanges)
+ changesByAccountId, sessionState, state, 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, lang)
+ return etagResponse(changesByAccountId, sessionState, state, lang)
})
}
@@ -336,12 +336,92 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
l.Array(logAccountId, log.SafeStringArray(allAccountIds))
logger := log.From(l)
- rolesByAccountId, sessionState, lang, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language())
+ rolesByAccountId, sessionState, state, lang, jerr := g.jmap.GetMailboxRolesForMultipleAccounts(allAccountIds, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
- return response(rolesByAccountId, sessionState, lang)
+ return etagResponse(rolesByAccountId, sessionState, state, lang)
+ })
+}
+
+func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
+ mailboxId := chi.URLParam(r, UriParamMailboxId)
+
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With().Str(UriParamMailboxId, log.SafeString(mailboxId))
+
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(err)
+ }
+ l = l.Str(logAccountId, accountId)
+
+ var body jmap.MailboxChange
+ err = req.body(&body)
+ if err != nil {
+ return errorResponse(err)
+ }
+ logger := log.From(l)
+
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, "", body)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return etagResponse(updated, sessionState, state, lang)
+ })
+}
+
+func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With()
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(err)
+ }
+ l = l.Str(logAccountId, accountId)
+
+ var body jmap.MailboxChange
+ err = req.body(&body)
+ if err != nil {
+ return errorResponse(err)
+ }
+ logger := log.From(l)
+
+ created, sessionState, state, lang, jerr := g.jmap.CreateMailbox(accountId, req.session, req.ctx, logger, req.language(), "", body)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return etagResponse(created, sessionState, state, lang)
+ })
+}
+
+func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
+ mailboxId := chi.URLParam(r, UriParamMailboxId)
+ mailboxIds := strings.Split(mailboxId, ",")
+
+ g.respond(w, r, func(req Request) Response {
+ if len(mailboxIds) < 1 {
+ return noContentResponse(req.session.State)
+ }
+
+ l := req.logger.With()
+ accountId, err := req.GetAccountIdForMail()
+ if err != nil {
+ return errorResponse(err)
+ }
+ l = l.Str(logAccountId, accountId)
+ l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
+ logger := log.From(l)
+
+ deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, req.session, req.ctx, logger, req.language(), "", mailboxIds)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return etagResponse(deleted, sessionState, state, lang)
})
}
@@ -360,13 +440,13 @@ func scoreMailbox(m jmap.Mailbox) int {
return 1000
}
-func sortMailboxesMap[K comparable](mailboxesByAccountId map[K]jmap.Mailboxes) map[K]jmap.Mailboxes {
- sortedByAccountId := make(map[K]jmap.Mailboxes, len(mailboxesByAccountId))
+func sortMailboxesMap[K comparable](mailboxesByAccountId map[K][]jmap.Mailbox) map[K][]jmap.Mailbox {
+ sortedByAccountId := make(map[K][]jmap.Mailbox, len(mailboxesByAccountId))
for accountId, unsorted := range mailboxesByAccountId {
- mailboxes := make([]jmap.Mailbox, len(unsorted.Mailboxes))
- copy(mailboxes, unsorted.Mailboxes)
+ mailboxes := make([]jmap.Mailbox, len(unsorted))
+ copy(mailboxes, unsorted)
slices.SortFunc(mailboxes, compareMailboxes)
- sortedByAccountId[accountId] = jmap.Mailboxes{Mailboxes: mailboxes, State: unsorted.State}
+ sortedByAccountId[accountId] = mailboxes
}
return sortedByAccountId
}
@@ -385,8 +465,14 @@ func compareMailboxes(a, b jmap.Mailbox) int {
// The number MUST be an integer in the range 0 <= sortOrder < 2^31.
// A Mailbox with a lower order should be displayed before a Mailbox with a higher order
// (that has the same parent) in any Mailbox listing in the client’s UI.
- sa := a.SortOrder
- sb := b.SortOrder
+ sa := 0
+ if a.SortOrder != nil {
+ sa = *a.SortOrder
+ }
+ sb := 0
+ if b.SortOrder != nil {
+ sb = *b.SortOrder
+ }
r := sa - sb
if r != 0 {
return r
diff --git a/services/groupware/pkg/groupware/groupware_api_quota.go b/services/groupware/pkg/groupware/groupware_api_quota.go
index bf0832abef..0731fad0e8 100644
--- a/services/groupware/pkg/groupware/groupware_api_quota.go
+++ b/services/groupware/pkg/groupware/groupware_api_quota.go
@@ -31,12 +31,12 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
- res, sessionState, lang, jerr := g.jmap.GetQuotas([]string{accountId}, req.session, req.ctx, logger, req.language())
+ res, sessionState, state, lang, jerr := g.jmap.GetQuotas([]string{accountId}, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
for _, v := range res {
- return etagResponse(v.List, sessionState, v.State, lang)
+ return etagResponse(v.List, sessionState, state, lang)
}
return notFoundResponse(sessionState)
})
@@ -70,7 +70,7 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
}
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
- res, sessionState, lang, jerr := g.jmap.GetQuotas(accountIds, req.session, req.ctx, logger, req.language())
+ res, sessionState, state, lang, jerr := g.jmap.GetQuotas(accountIds, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -82,6 +82,6 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
Quotas: accountQuotas.List,
}
}
- return response(result, sessionState, lang)
+ return etagResponse(result, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_api_vacation.go b/services/groupware/pkg/groupware/groupware_api_vacation.go
index a8b86bef35..e7c949c274 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, lang, jerr := g.jmap.GetVacationResponse(accountId, req.session, req.ctx, logger, req.language())
+ res, sessionState, state, 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, lang)
+ return etagResponse(res, sessionState, state, lang)
})
}
@@ -50,7 +50,7 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
type SwaggerSetVacationResponse200 struct {
// in: body
Body struct {
- *jmap.VacationResponseChange
+ *jmap.VacationResponse
}
}
@@ -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, lang, jerr := g.jmap.SetVacationResponse(accountId, body, req.session, req.ctx, logger, req.language())
+ res, sessionState, state, 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, lang)
+ return etagResponse(res, sessionState, state, lang)
})
}
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index 87ea007cb3..c15b1b69d4 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -12,7 +12,7 @@ var (
const (
UriParamAccountId = "accountid"
- UriParamMailboxId = "mailbox"
+ UriParamMailboxId = "mailboxid"
UriParamEmailId = "emailid"
UriParamIdentityId = "identityid"
UriParamBlobId = "blobid"
@@ -90,9 +90,12 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/quota", g.GetQuota)
r.Route("/mailboxes", func(r chi.Router) {
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
- r.Get("/{mailbox}", g.GetMailbox)
- r.Get("/{mailbox}/emails", g.GetAllEmailsInMailbox)
- r.Get("/{mailbox}/changes", g.GetMailboxChanges)
+ r.Get("/{mailboxid}", g.GetMailbox)
+ r.Get("/{mailboxid}/emails", g.GetAllEmailsInMailbox)
+ r.Get("/{mailboxid}/changes", g.GetMailboxChanges)
+ r.Post("/", g.CreateMailbox)
+ r.Patch("/{mailboxid}", g.UpdateMailbox)
+ r.Delete("/{mailboxid}", g.DeleteMailbox)
})
r.Route("/emails", func(r chi.Router) {
r.Get("/", g.GetEmails) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&...
diff --git a/services/groupware/pkg/groupware/groupware_test.go b/services/groupware/pkg/groupware/groupware_test.go
index a6e7fbd461..b78ee96b0a 100644
--- a/services/groupware/pkg/groupware/groupware_test.go
+++ b/services/groupware/pkg/groupware/groupware_test.go
@@ -48,13 +48,14 @@ func TestSanitizeEmail(t *testing.T) {
}
func TestSortMailboxes(t *testing.T) {
+ o := -10
mailboxes := []jmap.Mailbox{
{Id: "a", Name: "Other"},
{Id: "b", Role: jmap.JmapMailboxRoleSent, Name: "Sent"},
{Id: "c", Name: "Zebras"},
{Id: "d", Role: jmap.JmapMailboxRoleInbox, Name: "Inbox"},
{Id: "e", Name: "Appraisal"},
- {Id: "f", Name: "Zealots", SortOrder: -10},
+ {Id: "f", Name: "Zealots", SortOrder: &o},
}
slices.SortFunc(mailboxes, compareMailboxes)
names := structs.Map(mailboxes, func(m jmap.Mailbox) string { return m.Name })