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:
Pascal Bleser
2025-10-17 10:02:40 +02:00
parent a9efa44908
commit 9ef74191c0
11 changed files with 773 additions and 256 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,3 +116,11 @@ func notFoundResponse(sessionState jmap.SessionState) Response {
sessionState: sessionState,
}
}
func notImplementesResponse() Response {
return Response{
body: nil,
status: http.StatusNotImplemented,
err: nil,
}
}

View File

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