diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index 6b85beb4c4..3d62d48491 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -187,71 +187,125 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context. }) } -type EmailSnippetQueryResult struct { - Snippets []SearchSnippet `json:"snippets,omitempty"` - Total uint `json:"total"` - Limit uint `json:"limit,omitzero"` - Position uint `json:"position,omitzero"` - QueryState State `json:"queryState"` +type SearchSnippetWithMeta struct { + ReceivedAt time.Time `json:"receivedAt,omitzero"` + EmailId string `json:"emailId,omitempty"` + SearchSnippet } -func (j *Client) QueryEmailSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint) (EmailSnippetQueryResult, SessionState, Language, Error) { - logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { +type EmailSnippetQueryResult struct { + Snippets []SearchSnippetWithMeta `json:"snippets,omitempty"` + Total uint `json:"total"` + Limit uint `json:"limit,omitzero"` + Position uint `json:"position,omitzero"` + 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) { + logger = j.loggerParams("QueryEmailSnippets", session, logger, func(z zerolog.Context) zerolog.Context { return z.Uint(logLimit, limit).Uint(logOffset, offset) }) - query := EmailQueryCommand{ - AccountId: accountId, - Filter: filter, - Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, - CollapseThreads: true, - CalculateTotal: true, - } - if offset > 0 { - query.Position = offset - } - if limit > 0 { - query.Limit = limit + uniqueAccountIds := structs.Uniq(accountIds) + invocations := make([]Invocation, len(uniqueAccountIds)*3) + for i, accountId := range uniqueAccountIds { + query := EmailQueryCommand{ + AccountId: accountId, + Filter: filter, + Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, + CollapseThreads: true, + CalculateTotal: true, + } + if offset > 0 { + query.Position = offset + } + if limit > 0 { + query.Limit = limit + } + + mails := EmailGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + ResultOf: mcid(accountId, "0"), + Name: CommandEmailQuery, + Path: "/ids/*", + }, + FetchAllBodyValues: false, + MaxBodyValueBytes: 0, + Properties: []string{"id", "receivedAt", "sentAt"}, + } + + snippet := SearchSnippetGetRefCommand{ + AccountId: accountId, + Filter: filter, + EmailIdRef: &ResultReference{ + ResultOf: mcid(accountId, "0"), + Name: CommandEmailQuery, + Path: "/ids/*", + }, + } + + invocations[i*3+0] = invocation(CommandEmailQuery, query, mcid(accountId, "0")) + invocations[i*3+1] = invocation(CommandEmailGet, mails, mcid(accountId, "1")) + invocations[i*3+2] = invocation(CommandSearchSnippetGet, snippet, mcid(accountId, "2")) } - snippet := SearchSnippetGetRefCommand{ - AccountId: accountId, - Filter: filter, - EmailIdRef: &ResultReference{ - ResultOf: "0", - Name: CommandEmailQuery, - Path: "/ids/*", - }, - } - - cmd, err := j.request(session, logger, - invocation(CommandEmailQuery, query, "0"), - invocation(CommandSearchSnippetGet, snippet, "1"), - ) + cmd, err := j.request(session, logger, invocations...) if err != nil { - return EmailSnippetQueryResult{}, "", "", err + return nil, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailSnippetQueryResult, Error) { - var queryResponse EmailQueryResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) - if err != nil { - return EmailSnippetQueryResult{}, err - } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailSnippetQueryResult, 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 + } - var snippetResponse SearchSnippetGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, "1", &snippetResponse) - if err != nil { - return EmailSnippetQueryResult{}, err - } + var mailResponse EmailGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &mailResponse) + if err != nil { + return nil, err + } - return EmailSnippetQueryResult{ - Snippets: snippetResponse.List, - Total: queryResponse.Total, - Limit: queryResponse.Limit, - Position: queryResponse.Position, - QueryState: queryResponse.QueryState, - }, nil + var snippetResponse SearchSnippetGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, mcid(accountId, "2"), &snippetResponse) + if err != nil { + return nil, err + } + + mailResponseById := structs.Index(mailResponse.List, func(e Email) string { return e.Id }) + + snippets := make([]SearchSnippetWithMeta, len(queryResponse.Ids)) + if len(queryResponse.Ids) > len(snippetResponse.List) { + // TODO how do we handle this, if there are more email IDs than snippets? + } + + i := 0 + for _, id := range queryResponse.Ids { + if mail, ok := mailResponseById[id]; ok { + snippets[i] = SearchSnippetWithMeta{ + EmailId: id, + ReceivedAt: mail.ReceivedAt, + SearchSnippet: snippetResponse.List[i], + } + } else { + // TODO how do we handle this, if there is no email result for that id? + } + i++ + } + + results[accountId] = EmailSnippetQueryResult{ + Snippets: snippets, + Total: queryResponse.Total, + Limit: queryResponse.Limit, + Position: queryResponse.Position, + QueryState: queryResponse.QueryState, + } + } + return results, nil }) } @@ -263,64 +317,72 @@ type EmailQueryResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryResult, SessionState, Language, Error) { +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) { logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) - query := EmailQueryCommand{ - AccountId: accountId, - Filter: filter, - Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, - CollapseThreads: true, - CalculateTotal: true, - } - if offset > 0 { - query.Position = offset - } - if limit > 0 { - query.Limit = limit + uniqueAccountIds := structs.Uniq(accountIds) + invocations := make([]Invocation, len(uniqueAccountIds)*2) + for i, accountId := range uniqueAccountIds { + query := EmailQueryCommand{ + AccountId: accountId, + Filter: filter, + Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, + CollapseThreads: true, + CalculateTotal: true, + } + if offset > 0 { + query.Position = offset + } + if limit > 0 { + query.Limit = limit + } + + mails := EmailGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + ResultOf: mcid(accountId, "0"), + Name: CommandEmailQuery, + Path: "/ids/*", + }, + FetchAllBodyValues: fetchBodies, + MaxBodyValueBytes: maxBodyValueBytes, + } + + invocations[i*2+0] = invocation(CommandEmailQuery, query, mcid(accountId, "0")) + invocations[i*2+1] = invocation(CommandEmailGet, mails, mcid(accountId, "1")) } - mails := EmailGetRefCommand{ - AccountId: accountId, - IdsRef: &ResultReference{ - ResultOf: "0", - Name: CommandEmailQuery, - Path: "/ids/*", - }, - FetchAllBodyValues: fetchBodies, - MaxBodyValueBytes: maxBodyValueBytes, - } - - cmd, err := j.request(session, logger, - invocation(CommandEmailQuery, query, "0"), - invocation(CommandEmailGet, mails, "1"), - ) + cmd, err := j.request(session, logger, invocations...) if err != nil { - return EmailQueryResult{}, "", "", err + return nil, "", "", err } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailQueryResult, Error) { - var queryResponse EmailQueryResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) - if err != nil { - return EmailQueryResult{}, err - } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryResult, 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 + } - var emailsResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &emailsResponse) - if err != nil { - return EmailQueryResult{}, err - } + var emailsResponse EmailGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &emailsResponse) + if err != nil { + return nil, err + } - return EmailQueryResult{ - Emails: emailsResponse.List, - Total: queryResponse.Total, - Limit: queryResponse.Limit, - Position: queryResponse.Position, - QueryState: queryResponse.QueryState, - }, nil + results[accountId] = EmailQueryResult{ + Emails: emailsResponse.List, + Total: queryResponse.Total, + Limit: queryResponse.Limit, + Position: queryResponse.Position, + QueryState: queryResponse.QueryState, + } + } + return results, nil }) } @@ -337,104 +399,110 @@ type EmailQueryWithSnippetsResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (EmailQueryWithSnippetsResult, SessionState, Language, Error) { +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) { logger = j.loggerParams("QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) - query := EmailQueryCommand{ - AccountId: accountId, - Filter: filter, - Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, - CollapseThreads: false, - CalculateTotal: true, - } - if offset > 0 { - query.Position = offset - } - if limit > 0 { - query.Limit = limit + uniqueAccountIds := structs.Uniq(accountIds) + invocations := make([]Invocation, len(uniqueAccountIds)*3) + for i, accountId := range uniqueAccountIds { + query := EmailQueryCommand{ + AccountId: accountId, + Filter: filter, + Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}}, + CollapseThreads: false, + CalculateTotal: true, + } + if offset > 0 { + query.Position = offset + } + if limit > 0 { + query.Limit = limit + } + + snippet := SearchSnippetGetRefCommand{ + AccountId: accountId, + Filter: filter, + EmailIdRef: &ResultReference{ + ResultOf: mcid(accountId, "0"), + Name: CommandEmailQuery, + Path: "/ids/*", + }, + } + + mails := EmailGetRefCommand{ + AccountId: accountId, + IdsRef: &ResultReference{ + ResultOf: mcid(accountId, "0"), + Name: CommandEmailQuery, + Path: "/ids/*", + }, + FetchAllBodyValues: fetchBodies, + MaxBodyValueBytes: maxBodyValueBytes, + } + invocations[i*3+0] = invocation(CommandEmailQuery, query, mcid(accountId, "0")) + invocations[i*3+1] = invocation(CommandSearchSnippetGet, snippet, mcid(accountId, "1")) + invocations[i*3+2] = invocation(CommandEmailGet, mails, mcid(accountId, "2")) } - snippet := SearchSnippetGetRefCommand{ - AccountId: accountId, - Filter: filter, - EmailIdRef: &ResultReference{ - ResultOf: "0", - Name: CommandEmailQuery, - Path: "/ids/*", - }, - } - - mails := EmailGetRefCommand{ - AccountId: accountId, - IdsRef: &ResultReference{ - ResultOf: "0", - Name: CommandEmailQuery, - Path: "/ids/*", - }, - FetchAllBodyValues: fetchBodies, - MaxBodyValueBytes: maxBodyValueBytes, - } - - cmd, err := j.request(session, logger, - invocation(CommandEmailQuery, query, "0"), - invocation(CommandSearchSnippetGet, snippet, "1"), - invocation(CommandEmailGet, mails, "2"), - ) - + cmd, err := j.request(session, logger, invocations...) if err != nil { logger.Error().Err(err).Send() - return EmailQueryWithSnippetsResult{}, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) + return nil, "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload) } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailQueryWithSnippetsResult, Error) { - var queryResponse EmailQueryResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailQuery, "0", &queryResponse) - if err != nil { - return EmailQueryWithSnippetsResult{}, err - } - - var snippetResponse SearchSnippetGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, "1", &snippetResponse) - if err != nil { - return EmailQueryWithSnippetsResult{}, err - } - - var emailsResponse EmailGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "2", &emailsResponse) - if err != nil { - return EmailQueryWithSnippetsResult{}, err - } - - snippetsById := map[string][]SearchSnippet{} - for _, snippet := range snippetResponse.List { - list, ok := snippetsById[snippet.EmailId] - if !ok { - list = []SearchSnippet{} + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]EmailQueryWithSnippetsResult, 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 } - snippetsById[snippet.EmailId] = append(list, snippet) - } - results := []EmailWithSnippets{} - for _, email := range emailsResponse.List { - snippets, ok := snippetsById[email.Id] - if !ok { - snippets = []SearchSnippet{} + var snippetResponse SearchSnippetGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandSearchSnippetGet, mcid(accountId, "1"), &snippetResponse) + if err != nil { + return nil, err } - results = append(results, EmailWithSnippets{ - Email: email, - Snippets: snippets, - }) - } - return EmailQueryWithSnippetsResult{ - Results: results, - Total: queryResponse.Total, - Limit: queryResponse.Limit, - Position: queryResponse.Position, - QueryState: queryResponse.QueryState, - }, nil + var emailsResponse EmailGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "2"), &emailsResponse) + if err != nil { + return nil, err + } + + snippetsById := map[string][]SearchSnippet{} + for _, snippet := range snippetResponse.List { + list, ok := snippetsById[snippet.EmailId] + if !ok { + list = []SearchSnippet{} + } + snippetsById[snippet.EmailId] = append(list, snippet) + } + + results := []EmailWithSnippets{} + for _, email := range emailsResponse.List { + snippets, ok := snippetsById[email.Id] + if !ok { + snippets = []SearchSnippet{} + } + results = append(results, EmailWithSnippets{ + Email: email, + Snippets: snippets, + }) + } + + result[accountId] = EmailQueryWithSnippetsResult{ + Results: results, + Total: queryResponse.Total, + Limit: queryResponse.Limit, + Position: queryResponse.Position, + QueryState: queryResponse.QueryState, + } + } + return result, nil }) } diff --git a/pkg/jmap/jmap_api_identity.go b/pkg/jmap/jmap_api_identity.go index 0c095b4dc7..bb2465a1ca 100644 --- a/pkg/jmap/jmap_api_identity.go +++ b/pkg/jmap/jmap_api_identity.go @@ -13,9 +13,8 @@ type Identities struct { State State `json:"state"` } -// https://jmap.io/spec-mail.html#identityget -func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (Identities, SessionState, Language, Error) { - logger = j.logger("GetIdentity", session, logger) +func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (Identities, SessionState, 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 @@ -33,16 +32,35 @@ func (j *Client) GetIdentity(accountId string, session *Session, ctx context.Con }) } +func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) (Identities, SessionState, 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 command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (Identities, Error) { + var response IdentityGetResponse + err = retrieveResponseMatchParameters(logger, body, CommandIdentityGet, "0", &response) + if err != nil { + return Identities{}, err + } + return Identities{ + Identities: response.List, + State: 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) GetIdentities(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, Language, Error) { +func (j *Client) GetIdentitiesForAllAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (IdentitiesGetResponse, SessionState, Language, Error) { uniqueAccountIds := structs.Uniq(accountIds) - logger = j.logger("GetIdentities", session, logger) + logger = j.logger("GetIdentitiesForAllAccounts", session, logger) calls := make([]Invocation, len(uniqueAccountIds)) for i, accountId := range uniqueAccountIds { @@ -129,3 +147,55 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId string, accountIds [ }, nil }) } + +func (j *Client) CreateIdentity(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identity Identity) (State, SessionState, Language, Error) { + logger = j.logger("CreateIdentity", session, logger) + cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{ + AccountId: accountId, + Create: map[string]Identity{ + "c": identity, + }, + }, "0")) + if err != nil { + return "", "", "", err + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (State, Error) { + var response IdentitySetResponse + err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response) + if err != nil { + return 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 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) { + logger = j.logger("UpdateIdentity", session, logger) + cmd, err := j.request(session, logger, invocation(CommandIdentitySet, IdentitySetCommand{ + AccountId: accountId, + Update: map[string]PatchObject{ + "c": identity.AsPatch(), + }, + }, "0")) + if err != nil { + return "", "", "", err + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (State, Error) { + var response IdentitySetResponse + err = retrieveResponseMatchParameters(logger, body, CommandIdentitySet, "0", &response) + if err != nil { + return 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 response.NewState, nil + }) +} diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go index ecafcf1d75..fb81113057 100644 --- a/pkg/jmap/jmap_integration_test.go +++ b/pkg/jmap/jmap_integration_test.go @@ -724,7 +724,7 @@ func TestEmails(t *testing.T) { { { - resp, sessionState, _, err := s.client.GetIdentity(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) diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 5eeeefef1d..bb81e3c57b 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -3025,9 +3025,29 @@ type IdentityGetCommand struct { Ids []string `json:"ids,omitempty"` } +type IdentitySetCommand struct { + AccountId string `json:"accountId"` + IfInState string `json:"ifInState,omitempty"` + Create map[string]Identity `json:"create,omitempty"` + Update map[string]PatchObject `json:"update,omitempty"` + Destroy []string `json:"destroy,omitempty"` +} + +type IdentitySetResponse struct { + AccountId string `json:"accountId"` + OldState State `json:"oldState,omitempty"` + NewState State `json:"newState,omitempty"` + Created map[string]Identity `json:"created,omitempty"` + Updated map[string]Identity `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 Identity struct { // The id of the Identity. - Id string `json:"id"` + Id string `json:"id,omitempty"` // The “From” name the client SHOULD use when creating a new Email from this Identity. Name string `json:"name,omitempty"` @@ -3043,13 +3063,13 @@ type Identity struct { ReplyTo string `json:"replyTo,omitempty"` // The Bcc value the client SHOULD set when creating a new Email from this Identity. - Bcc []EmailAddress `json:"bcc,omitempty"` + Bcc *[]EmailAddress `json:"bcc,omitempty"` // A signature the client SHOULD insert into new plaintext messages that will be sent from // this Identity. // // Clients MAY ignore this and/or combine this with a client-specific signature preference. - TextSignature string `json:"textSignature,omitempty"` + TextSignature *string `json:"textSignature,omitempty"` // A signature the client SHOULD insert into new HTML messages that will be sent from this // Identity. @@ -3057,7 +3077,7 @@ type Identity struct { // This text MUST be an HTML snippet to be inserted into the section of the HTML. // // Clients MAY ignore this and/or combine this with a client-specific signature preference. - HtmlSignature string `json:"htmlSignature,omitempty"` + HtmlSignature *string `json:"htmlSignature,omitempty"` // Is the user allowed to delete this Identity? // @@ -3065,7 +3085,30 @@ type Identity struct { // // Attempts to destroy an Identity with mayDelete: false will be rejected with a standard // forbidden SetError. - MayDelete bool `json:"mayDelete"` + MayDelete bool `json:"mayDelete,omitzero"` +} + +func (i Identity) AsPatch() PatchObject { + p := PatchObject{} + if i.Name != "" { + p["name"] = i.Name + } + if i.Email != "" { + p["email"] = i.Email + } + if i.ReplyTo != "" { + p["replyTo"] = i.ReplyTo + } + if i.Bcc != nil { + p["bcc"] = i.Bcc + } + if i.TextSignature != nil { + p["textSignature"] = i.TextSignature + } + if i.HtmlSignature != nil { + p["htmlSignature"] = i.HtmlSignature + } + return p } type IdentityGetResponse struct { @@ -4639,6 +4682,7 @@ const ( CommandMailboxQuery Command = "Mailbox/query" CommandMailboxChanges Command = "Mailbox/changes" CommandIdentityGet Command = "Identity/get" + CommandIdentitySet Command = "Identity/set" CommandVacationResponseGet Command = "VacationResponse/get" CommandVacationResponseSet Command = "VacationResponse/set" CommandSearchSnippetGet Command = "SearchSnippet/get" @@ -4660,6 +4704,7 @@ var CommandResponseTypeMap = map[Command]func() any{ CommandEmailSubmissionSet: func() any { return EmailSubmissionSetResponse{} }, CommandThreadGet: func() any { return ThreadGetResponse{} }, CommandIdentityGet: func() any { return IdentityGetResponse{} }, + CommandIdentitySet: func() any { return IdentitySetResponse{} }, CommandVacationResponseGet: func() any { return VacationResponseGetResponse{} }, CommandVacationResponseSet: func() any { return VacationResponseSetResponse{} }, CommandSearchSnippetGet: func() any { return SearchSnippetGetResponse{} }, diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 6c7558789f..d9c30e6082 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -66,3 +66,18 @@ func Map[E any, R any](source []E, indexer func(E) R) []R { } return result } + +func MapN[E any, R any](source []E, indexer func(E) *R) []R { + if source == nil { + var zero []R + return zero + } + result := []R{} + for _, e := range source { + opt := indexer(e) + if opt != nil { + result = append(result, *opt) + } + } + return result +} diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go index fdeabc3008..5576885aac 100644 --- a/services/groupware/pkg/groupware/groupware_api_emails.go +++ b/services/groupware/pkg/groupware/groupware_api_emails.go @@ -5,7 +5,7 @@ import ( "fmt" "io" "net/http" - "sort" + "slices" "strconv" "strings" "time" @@ -312,30 +312,43 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since } type EmailSearchSnippetsResults struct { - Results []jmap.SearchSnippet `json:"results,omitempty"` - Total uint `json:"total,omitzero"` - Limit uint `json:"limit,omitzero"` - QueryState jmap.State `json:"queryState,omitempty"` + Results []Snippet `json:"results,omitempty"` + Total uint `json:"total,omitzero"` + Limit uint `json:"limit,omitzero"` + QueryState jmap.State `json:"queryState,omitempty"` } type EmailWithSnippets struct { + AccountId string `json:"accountId,omitempty"` jmap.Email Snippets []SnippetWithoutEmailId `json:"snippets,omitempty"` } +type Snippet struct { + AccountId string `json:"accountId,omitempty"` + jmap.SearchSnippetWithMeta +} + type SnippetWithoutEmailId struct { Subject string `json:"subject,omitempty"` Preview string `json:"preview,omitempty"` } -type EmailSearchResults struct { +type EmailWithSnippetsSearchResults struct { Results []EmailWithSnippets `json:"results"` Total uint `json:"total,omitzero"` Limit uint `json:"limit,omitzero"` QueryState jmap.State `json:"queryState,omitempty"` } -func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uint, uint, *log.Logger, *Error) { +type EmailSearchResults struct { + Results []jmap.Email `json:"results"` + Total uint `json:"total,omitzero"` + Limit uint `json:"limit,omitzero"` + QueryState jmap.State `json:"queryState,omitempty"` +} + +func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, bool, uint, uint, *log.Logger, *Error) { q := req.r.URL.Query() mailboxId := q.Get(QueryParamMailboxId) notInMailboxIds := q[QueryParamNotInMailboxId] @@ -348,11 +361,13 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin body := q.Get(QueryParamSearchBody) keywords := q[QueryParamSearchKeyword] + snippets := false + l := req.logger.With() offset, ok, err := req.parseUIntParam(QueryParamOffset, 0) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Uint(QueryParamOffset, offset) @@ -360,7 +375,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaultEmailLimit) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Uint(QueryParamLimit, limit) @@ -368,7 +383,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin before, ok, err := req.parseDateParam(QueryParamSearchBefore) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Time(QueryParamSearchBefore, before) @@ -376,7 +391,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin after, ok, err := req.parseDateParam(QueryParamSearchAfter) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Time(QueryParamSearchAfter, after) @@ -412,7 +427,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Int(QueryParamSearchMinSize, minSize) @@ -420,7 +435,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0) if err != nil { - return false, nil, 0, 0, nil, err + return false, nil, snippets, 0, 0, nil, err } if ok { l = l.Int(QueryParamSearchMaxSize, maxSize) @@ -447,6 +462,10 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin } filter = &firstFilter + if text != "" || subject != "" || body != "" { + snippets = true + } + if len(keywords) > 0 { firstFilter.HasKeyword = keywords[0] if len(keywords) > 1 { @@ -462,12 +481,12 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, uin } } - return true, filter, offset, limit, logger, nil + return true, filter, snippets, offset, limit, logger, nil } func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - ok, filter, offset, limit, logger, err := g.buildFilter(req) + ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req) if !ok { return errorResponse(err) } @@ -499,32 +518,44 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { } logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId))) - results, sessionState, lang, jerr := g.jmap.QueryEmailsWithSnippets(accountId, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes) + g.jmap.QueryEmails([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit, fetchBodies, g.maxBodyValueBytes) + + resultsByAccount, sessionState, 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) } - flattened := make([]EmailWithSnippets, len(results.Results)) - for i, result := range results.Results { - snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) - for j, snippet := range result.Snippets { - snippets[j] = SnippetWithoutEmailId{ - Subject: snippet.Subject, - Preview: snippet.Preview, + if results, ok := resultsByAccount[accountId]; ok { + flattened := make([]EmailWithSnippets, len(results.Results)) + for i, result := range results.Results { + var snippets []SnippetWithoutEmailId + if makesSnippets { + snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) + for j, snippet := range result.Snippets { + snippets[j] = SnippetWithoutEmailId{ + Subject: snippet.Subject, + Preview: snippet.Preview, + } + } + } else { + snippets = nil + } + flattened[i] = EmailWithSnippets{ + // AccountId: accountId, + Email: result.Email, + Snippets: snippets, } } - flattened[i] = EmailWithSnippets{ - Email: result.Email, - Snippets: snippets, - } - } - return etagResponse(EmailSearchResults{ - Results: flattened, - Total: results.Total, - Limit: results.Limit, - QueryState: results.QueryState, - }, sessionState, results.QueryState, lang) + return etagResponse(EmailWithSnippetsSearchResults{ + Results: flattened, + Total: results.Total, + Limit: results.Limit, + QueryState: results.QueryState, + }, sessionState, results.QueryState, lang) + } else { + return notFoundResponse(sessionState) + } } else { accountId, err := req.GetAccountIdForMail() if err != nil { @@ -532,17 +563,21 @@ func (g *Groupware) searchEmails(w http.ResponseWriter, r *http.Request) { } logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId))) - results, sessionState, lang, jerr := g.jmap.QueryEmailSnippets(accountId, filter, req.session, req.ctx, logger, req.language(), offset, limit) + resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmailSnippets([]string{accountId}, filter, req.session, req.ctx, logger, req.language(), offset, limit) if jerr != nil { return req.errorResponseFromJmap(jerr) } - return etagResponse(EmailSearchSnippetsResults{ - Results: results.Snippets, - Total: results.Total, - Limit: results.Limit, - QueryState: results.QueryState, - }, sessionState, results.QueryState, lang) + if results, ok := resultsByAccountId[accountId]; ok { + return etagResponse(EmailSearchSnippetsResults{ + Results: structs.Map(results.Snippets, func(s jmap.SearchSnippetWithMeta) Snippet { return Snippet{SearchSnippetWithMeta: s} }), + Total: results.Total, + Limit: results.Limit, + QueryState: results.QueryState, + }, sessionState, results.QueryState, lang) + } else { + return notFoundResponse(sessionState) + } } }) } @@ -562,6 +597,175 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { } } +func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req) + if !ok { + return errorResponse(err) + } + + if !filter.IsNotEmpty() { + filter = nil + } + + fetchEmails, ok, err := req.parseBoolParam(QueryParamSearchFetchEmails, false) + if err != nil { + return errorResponse(err) + } + if ok { + logger = log.From(logger.With().Bool(QueryParamSearchFetchEmails, fetchEmails)) + } + + allAccountIds := structs.Keys(req.session.Accounts) // TODO(pbleser-oc) do we need a limit for a maximum amount of accounts to query at once? + logger = log.From(logger.With().Array(logAccountId, log.SafeStringArray(allAccountIds))) + + if fetchEmails { + fetchBodies, ok, err := req.parseBoolParam(QueryParamSearchFetchBodies, false) + if err != nil { + return errorResponse(err) + } + if ok { + logger = log.From(logger.With().Bool(QueryParamSearchFetchBodies, fetchBodies)) + } + + if makesSnippets { + resultsByAccountId, sessionState, 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) + } + + flattenedByAccountId := make(map[string][]EmailWithSnippets, len(resultsByAccountId)) + total := 0 + var totalOverAllAccounts uint = 0 + for accountId, results := range resultsByAccountId { + totalOverAllAccounts += results.Total + flattened := make([]EmailWithSnippets, len(results.Results)) + for i, result := range results.Results { + snippets := structs.MapN(result.Snippets, func(s jmap.SearchSnippet) *SnippetWithoutEmailId { + if s.Subject != "" || s.Preview != "" { + return &SnippetWithoutEmailId{ + Subject: s.Subject, + Preview: s.Preview, + } + } else { + return nil + } + }) + flattened[i] = EmailWithSnippets{ + AccountId: accountId, + Email: result.Email, + Snippets: snippets, + } + } + flattenedByAccountId[accountId] = flattened + total += len(flattened) + } + + flattened := make([]EmailWithSnippets, total) + { + i := 0 + for _, list := range flattenedByAccountId { + for _, e := range list { + flattened[i] = e + i++ + } + } + } + + 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 + + return etagResponse(EmailWithSnippetsSearchResults{ + Results: flattened, + Total: totalOverAllAccounts, + Limit: limit, + QueryState: squashedQueryState, + }, sessionState, squashedQueryState, lang) + } else { + resultsByAccountId, sessionState, 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) + } + + total := 0 + var totalOverAllAccounts uint = 0 + for _, results := range resultsByAccountId { + totalOverAllAccounts += results.Total + total += len(results.Emails) + } + + flattened := make([]jmap.Email, total) + { + i := 0 + for _, list := range resultsByAccountId { + for _, e := range list.Emails { + flattened[i] = e + i++ + } + } + } + + 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 + + return etagResponse(EmailSearchResults{ + Results: flattened, + Total: totalOverAllAccounts, + Limit: limit, + QueryState: squashedQueryState, + }, sessionState, squashedQueryState, lang) + } + } else { + if makesSnippets { + resultsByAccountId, sessionState, lang, jerr := g.jmap.QueryEmailSnippets(allAccountIds, filter, req.session, req.ctx, logger, req.language(), offset, limit) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + + var totalOverAllAccounts uint = 0 + total := 0 + for _, results := range resultsByAccountId { + totalOverAllAccounts += results.Total + total += len(results.Snippets) + } + + flattened := make([]Snippet, total) + { + i := 0 + for accountId, results := range resultsByAccountId { + for _, result := range results.Snippets { + flattened[i] = Snippet{ + AccountId: accountId, + SearchSnippetWithMeta: result, + } + } + } + } + + slices.SortFunc(flattened, func(a, b Snippet) int { return a.ReceivedAt.Compare(b.ReceivedAt) }) + + // 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) + } else { + // TODO implement search without email bodies (only retrieve a few chosen properties?) + without snippets + return notImplementesResponse() + } + } + }) +} + type EmailCreation struct { MailboxIds []string `json:"mailboxIds,omitempty"` Keywords []string `json:"keywords,omitempty"` @@ -1086,17 +1290,19 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { g.job(logger, RelationTypeSameSender, func(jobId uint64, l *log.Logger) { before := time.Now() - results, _, lang, jerr := g.jmap.QueryEmails(accountId, filter, req.session, bgctx, l, req.language(), 0, limit, false, g.maxBodyValueBytes) - duration := time.Since(before) - if jerr != nil { - req.observeJmapError(jerr) - l.Error().Err(jerr).Msgf("failed to query %v emails", RelationTypeSameSender) - } else { - req.observe(g.metrics.EmailSameSenderDuration.WithLabelValues(req.session.JmapEndpoint), duration.Seconds()) - related := filterEmails(results.Emails, email) - l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameSender, len(related)) - if len(related) > 0 { - req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender, Language: lang}) + 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 { + req.observeJmapError(jerr) + l.Error().Err(jerr).Msgf("failed to query %v emails", RelationTypeSameSender) + } else { + req.observe(g.metrics.EmailSameSenderDuration.WithLabelValues(req.session.JmapEndpoint), duration.Seconds()) + related := filterEmails(results.Emails, email) + l.Trace().Msgf("'%v' found %v other emails", RelationTypeSameSender, len(related)) + if len(related) > 0 { + req.push(RelationEntityEmail, AboutEmailsEvent{Id: reqId, Emails: related, Source: RelationTypeSameSender, Language: lang}) + } } } }) @@ -1433,9 +1639,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, } } - sort.Slice(all, func(i int, j int) bool { - return all[i].email.ReceivedAt.Before(all[j].email.ReceivedAt) - }) + slices.SortFunc(all, func(a, b emailWithAccountId) int { return -(a.email.ReceivedAt.Compare(b.email.ReceivedAt)) }) summaries := make([]EmailSummary, min(limit, total)) for i = 0; i < limit && i < total; i++ { @@ -1470,3 +1674,32 @@ func filterFromNotKeywords(keywords []string) jmap.EmailFilterElement { return jmap.EmailFilterOperator{Operator: jmap.And, Conditions: conditions} } } + +func squashQueryState[V any](all map[string]V, mapper func(V) jmap.State) jmap.State { + n := len(all) + if n == 0 { + return jmap.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 jmap.State(strings.Join(parts, ",")) +} diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go index 1cdd1618b2..a6a2bc98c7 100644 --- a/services/groupware/pkg/groupware/groupware_api_identity.go +++ b/services/groupware/pkg/groupware/groupware_api_identity.go @@ -3,6 +3,7 @@ package groupware import ( "net/http" + "github.com/go-chi/chi/v5" "github.com/opencloud-eu/opencloud/pkg/jmap" "github.com/opencloud-eu/opencloud/pkg/log" ) @@ -27,15 +28,78 @@ type SwaggerGetIdentitiesResponse struct { // 500: ErrorResponse500 func (g *Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - accountId, err := req.GetAccountIdWithoutFallback() + accountId, err := req.GetAccountIdForMail() if err != nil { return errorResponse(err) } logger := log.From(req.logger.With().Str(logAccountId, accountId)) - res, sessionState, lang, jerr := g.jmap.GetIdentity(accountId, req.session, req.ctx, logger, req.language()) + res, sessionState, 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) }) } + +func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + accountId, err := req.GetAccountIdWithoutFallback() + if err != nil { + return errorResponse(err) + } + 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}) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + if len(res.Identities) < 1 { + return notFoundResponse(sessionState) + } + return etagResponse(res.Identities[0], sessionState, res.State, lang) + }) +} + +func (g *Groupware) AddIdentity(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + accountId, err := req.GetAccountIdWithoutFallback() + if err != nil { + return errorResponse(err) + } + logger := log.From(req.logger.With().Str(logAccountId, accountId)) + + var identity jmap.Identity + err = req.body(&identity) + if err != nil { + return errorResponse(err) + } + + newState, sessionState, _, jerr := g.jmap.CreateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + return noContentResponseWithEtag(sessionState, newState) + }) +} + +func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + accountId, err := req.GetAccountIdWithoutFallback() + if err != nil { + return errorResponse(err) + } + logger := log.From(req.logger.With().Str(logAccountId, accountId)) + + var identity jmap.Identity + err = req.body(&identity) + if err != nil { + return errorResponse(err) + } + + newState, sessionState, _, jerr := g.jmap.UpdateIdentity(accountId, req.session, req.ctx, logger, req.language(), identity) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + return noContentResponseWithEtag(sessionState, newState) + }) +} diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go index 63d1d6547c..33613521ec 100644 --- a/services/groupware/pkg/groupware/groupware_framework.go +++ b/services/groupware/pkg/groupware/groupware_framework.go @@ -43,6 +43,7 @@ const ( logInvalidQueryParameter = "error-query-param" logInvalidPathParameter = "error-path-param" logFolderId = "folder-id" + logIdentityId = "identity-id" logQuery = "query" logEmailId = "email-id" logJobDescription = "job" diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go index ea21dca348..56d6b67399 100644 --- a/services/groupware/pkg/groupware/groupware_request.go +++ b/services/groupware/pkg/groupware/groupware_request.go @@ -123,15 +123,21 @@ func (r Request) GetAccountIdForSubmission() (string, *Error) { } func (r Request) GetAccountIdForTask() (string, *Error) { - return r.getAccountId(r.session.PrimaryAccounts.Task, errNoPrimaryAccountForTask) + // TODO we don't have these yet, not implemented in Stalwart + // return r.getAccountId(r.session.PrimaryAccounts.Task, errNoPrimaryAccountForTask) + return r.GetAccountIdForMail() } func (r Request) GetAccountIdForCalendar() (string, *Error) { - return r.getAccountId(r.session.PrimaryAccounts.Calendar, errNoPrimaryAccountForCalendar) + // TODO we don't have these yet, not implemented in Stalwart + // return r.getAccountId(r.session.PrimaryAccounts.Calendar, errNoPrimaryAccountForCalendar) + return r.GetAccountIdForMail() } func (r Request) GetAccountIdForContact() (string, *Error) { - return r.getAccountId(r.session.PrimaryAccounts.Contact, errNoPrimaryAccountForContact) + // TODO we don't have these yet, not implemented in Stalwart + // return r.getAccountId(r.session.PrimaryAccounts.Contact, errNoPrimaryAccountForContact) + return r.GetAccountIdForMail() } func (r Request) GetAccountForMail() (jmap.Account, *Error) { diff --git a/services/groupware/pkg/groupware/groupware_response.go b/services/groupware/pkg/groupware/groupware_response.go index 51fec8c418..a2f283eec4 100644 --- a/services/groupware/pkg/groupware/groupware_response.go +++ b/services/groupware/pkg/groupware/groupware_response.go @@ -116,3 +116,11 @@ func notFoundResponse(sessionState jmap.SessionState) Response { sessionState: sessionState, } } + +func notImplementesResponse() Response { + return Response{ + body: nil, + status: http.StatusNotImplemented, + err: nil, + } +} diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 17f49ffe69..40515201e8 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -14,6 +14,7 @@ const ( UriParamAccountId = "accountid" UriParamMailboxId = "mailbox" UriParamEmailId = "emailid" + UriParamIdentityId = "identityid" UriParamBlobId = "blobid" UriParamBlobName = "blobname" UriParamStreamId = "stream" @@ -66,13 +67,19 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/roles/{role}", g.GetMailboxByRoleForAllAccounts) // ?role= }) r.Route("/emails", func(r chi.Router) { + r.Get("/", g.GetEmailsForAllAccounts) r.Get("/latest/summary", g.GetLatestEmailsSummaryForAllAccounts) // ?limit=10&seen=true&undesirable=true }) r.Get("/quota", g.GetQuotaForAllAccounts) }) r.Route("/accounts/{accountid}", func(r chi.Router) { r.Get("/", g.GetAccount) - r.Get("/identities", g.GetIdentities) + r.Route("/identities", func(r chi.Router) { + r.Get("/", g.GetIdentities) + r.Get("/{identityid}", g.GetIdentityById) + r.Post("/", g.AddIdentity) + r.Patch("/{identityid}", g.ModifyIdentity) + }) r.Get("/vacation", g.GetVacation) r.Put("/vacation", g.SetVacation) r.Get("/quota", g.GetQuota)