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
}