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