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
This commit is contained in:
Pascal Bleser
2025-10-28 17:53:45 +01:00
parent ee399503ba
commit 3c19e1d1db
26 changed files with 801 additions and 531 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ const (
JmapErrorWssFailedToSendWebSocketPushDisable
JmapErrorWssFailedToClose
JmapErrorWssFailedToRetrieveSession
JmapErrorMissingCreatedObject
)
var (

View File

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

View File

@@ -1201,7 +1201,7 @@ type Mailbox struct {
// (that has the same parent) in any Mailbox listing in the clients 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 clients 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 clients 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 users 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{} },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,11 +37,11 @@ func (g *Groupware) GetVacation(w http.ResponseWriter, r *http.Request) {
}
logger := log.From(req.logger.With().Str(logAccountId, accountId))
res, sessionState, 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)
})
}

View File

@@ -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=&...

View File

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