mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-06 12:21:21 -05:00
groupware:
* made several email related operations multi-account: QueryEmailSnippets, QueryEmails, QueryEmailsWithSnippets * add GetIdentitiesForAllAccounts * add GetEmailsForAllAccounts * jmap: add CreateIdentity, UpdateIdentity; groupware: add GetIdentityById, AddIdentity, ModifyIdentity * add temporary workaround until Calendars, Tasks, Contacts are implemented in Stalwart when determining the default account for those: use the mail one in the mean time
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <body></body> 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{} },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, ","))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -116,3 +116,11 @@ func notFoundResponse(sessionState jmap.SessionState) Response {
|
||||
sessionState: sessionState,
|
||||
}
|
||||
}
|
||||
|
||||
func notImplementesResponse() Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNotImplemented,
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user