mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-06-17 20:38:49 -04:00
Purpose is to make APIs and parameters easier to understand, since plain strings are used all over the place for all sorts of identifiers.
1160 lines
37 KiB
Go
1160 lines
37 KiB
Go
package jmap
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/pkg/structs"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
var NS_MAIL = ns(JmapMail)
|
|
var NS_MAIL_SUBMISSION = ns(JmapMail, JmapSubmission)
|
|
|
|
// Retrieve specific Emails by their id.
|
|
func (j *Client) GetEmails(accountId AccountId, ids []string, //NOSONAR
|
|
fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool,
|
|
ctx Context) (Result[EmailGetResponse], error) {
|
|
logger := j.logger("GetEmails", ctx)
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
getEmails := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
|
|
if maxBodyValueBytes > 0 {
|
|
getEmails.MaxBodyValueBytes = maxBodyValueBytes
|
|
}
|
|
invokeGet := invocation(getEmails, "1")
|
|
|
|
methodCalls := []Invocation{invokeGet}
|
|
var markEmails EmailSetCommand
|
|
if markAsSeen {
|
|
updates := make(map[string]PatchObject, len(ids))
|
|
for _, id := range ids {
|
|
updates[id] = PatchObject{EmailPropertyKeywords + "/" + JmapKeywordSeen: true}
|
|
}
|
|
markEmails = EmailSetCommand{AccountId: accountId, Update: updates}
|
|
methodCalls = []Invocation{invocation(markEmails, "0"), invokeGet}
|
|
}
|
|
var getThreads ThreadGetRefCommand
|
|
if withThreads {
|
|
getThreads = ThreadGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
ResultOf: "1",
|
|
Name: CommandEmailGet,
|
|
Path: "/list/*/" + EmailPropertyThreadId, //NOSONAR
|
|
},
|
|
}
|
|
methodCalls = append(methodCalls, invocation(getThreads, "2"))
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL, methodCalls...)
|
|
if err != nil {
|
|
return ZeroResult[EmailGetResponse](), err
|
|
}
|
|
return command(j, ctx, cmd, func(body *Response) (EmailGetResponse, State, Error) {
|
|
if markAsSeen {
|
|
var markResponse EmailSetResponse
|
|
err = retrieveSet(ctx, body, markEmails, "0", &markResponse)
|
|
if err != nil {
|
|
return EmailGetResponse{}, "", err
|
|
}
|
|
for _, seterr := range markResponse.NotUpdated {
|
|
// TODO we don't have a way to compose multiple set errors yet
|
|
return EmailGetResponse{}, "", setErrorError(seterr, EmailType)
|
|
}
|
|
}
|
|
var response EmailGetResponse
|
|
err = retrieveGet(ctx, body, getEmails, "1", &response)
|
|
if err != nil {
|
|
return EmailGetResponse{}, "", err
|
|
}
|
|
if withThreads {
|
|
var threads ThreadGetResponse
|
|
err = retrieveGet(ctx, body, getThreads, "2", &threads)
|
|
if err != nil {
|
|
return EmailGetResponse{}, "", err
|
|
}
|
|
setThreadSize(&threads, response.List)
|
|
}
|
|
return response, response.State, nil
|
|
})
|
|
}
|
|
|
|
func (j *Client) GetEmailBlobId(accountId AccountId, id string, ctx Context) (Result[string], error) {
|
|
logger := j.logger("GetEmailBlobId", ctx)
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}}
|
|
cmd, err := j.request(ctx, NS_MAIL, invocation(get, "0"))
|
|
if err != nil {
|
|
return ZeroResult[string](), err
|
|
}
|
|
return command(j, ctx, cmd, func(body *Response) (string, State, Error) {
|
|
var response EmailGetResponse
|
|
err = retrieveGet(ctx, body, get, "0", &response)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if len(response.List) != 1 {
|
|
return "", "", nil
|
|
}
|
|
email := response.List[0]
|
|
return email.BlobId, response.State, nil
|
|
})
|
|
}
|
|
|
|
type EmailSearchResults SearchResultsTemplate[Email]
|
|
|
|
var _ SearchResults[Email] = &EmailSearchResults{}
|
|
|
|
func (r *EmailSearchResults) GetResults() []Email { return r.Results }
|
|
func (r *EmailSearchResults) GetCanCalculateChanges() ChangeCalculation { return r.CanCalculateChanges }
|
|
func (r *EmailSearchResults) GetPosition() *uint { return r.Position }
|
|
func (r *EmailSearchResults) GetLimit() *uint { return r.Limit }
|
|
func (r *EmailSearchResults) GetTotal() *uint { return r.Total }
|
|
func (r *EmailSearchResults) RemoveResults() { r.Results = nil }
|
|
func (r *EmailSearchResults) SetLimit(limit *uint) { r.Limit = limit }
|
|
func (r *EmailSearchResults) SetPosition(position *uint) { r.Position = position }
|
|
|
|
// Retrieve all the Emails in a given Mailbox by its id.
|
|
func (j *Client) GetAllEmailsInMailbox(accountId AccountId, mailboxId string, //NOSONAR
|
|
qp QueryParams, limit *uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool,
|
|
ctx Context) (Result[*EmailSearchResults], error) {
|
|
logger := j.loggerParams("GetAllEmailsInMailbox", ctx, func(z zerolog.Context) zerolog.Context {
|
|
l := z.Bool(logFetchBodies, fetchBodies)
|
|
if limit != nil {
|
|
l = l.Uint(logLimit, *limit)
|
|
}
|
|
return l
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
query := EmailQueryCommand{
|
|
AccountId: accountId,
|
|
Filter: &EmailFilterCondition{InMailbox: mailboxId},
|
|
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
|
CollapseThreads: collapseThreads,
|
|
CalculateTotal: true,
|
|
Position: qp.Position,
|
|
Anchor: qp.Anchor,
|
|
AnchorOffset: qp.AnchorOffset,
|
|
Limit: limit,
|
|
}
|
|
|
|
get := EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
FetchAllBodyValues: fetchBodies,
|
|
IdsRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"}, //NOSONAR
|
|
}
|
|
if maxBodyValueBytes > 0 {
|
|
get.MaxBodyValueBytes = maxBodyValueBytes
|
|
}
|
|
|
|
invocations := []Invocation{
|
|
invocation(query, "0"),
|
|
invocation(get, "1"),
|
|
}
|
|
|
|
threads := ThreadGetRefCommand{}
|
|
if withThreads {
|
|
threads = ThreadGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
ResultOf: "1",
|
|
Name: CommandEmailGet,
|
|
Path: "/list/*/" + EmailPropertyThreadId,
|
|
},
|
|
}
|
|
invocations = append(invocations, invocation(threads, "2"))
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL, invocations...)
|
|
if err != nil {
|
|
return ZeroResult[*EmailSearchResults](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (*EmailSearchResults, State, Error) {
|
|
var queryResponse *EmailQueryResponse
|
|
err = retrieveQuery(ctx, body, query, "0", &queryResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
var getResponse EmailGetResponse
|
|
err = retrieveGet(ctx, body, get, "1", &getResponse)
|
|
if err != nil {
|
|
logger.Error().Err(err).Send()
|
|
return nil, "", err
|
|
}
|
|
|
|
if withThreads {
|
|
var thread ThreadGetResponse
|
|
err = retrieveGet(ctx, body, threads, "2", &thread)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
setThreadSize(&thread, getResponse.List)
|
|
}
|
|
|
|
return &EmailSearchResults{
|
|
Results: getResponse.List,
|
|
CanCalculateChanges: ChangeCalculation(queryResponse.CanCalculateChanges),
|
|
Position: valueIf(queryResponse.Position, qp.Anchor == ""),
|
|
Limit: valueIf(queryResponse.Limit, limit != nil),
|
|
Total: uintPtr(queryResponse.Total),
|
|
}, queryResponse.QueryState, nil
|
|
})
|
|
}
|
|
|
|
type EmailChanges ChangesTemplate[Email]
|
|
|
|
var _ Changes[Email] = EmailChanges{}
|
|
|
|
func (c EmailChanges) GetHasMoreChanges() bool { return c.HasMoreChanges }
|
|
func (c EmailChanges) GetOldState() State { return c.OldState }
|
|
func (c EmailChanges) GetNewState() State { return c.NewState }
|
|
func (c EmailChanges) GetCreated() []Email { return c.Created }
|
|
func (c EmailChanges) GetUpdated() []Email { return c.Updated }
|
|
func (c EmailChanges) GetDestroyed() []string { return c.Destroyed }
|
|
|
|
// Retrieve the changes in Emails since a given State.
|
|
// @api:tags email,changes
|
|
func (j *Client) GetEmailChanges(accountId AccountId,
|
|
sinceState State, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint,
|
|
ctx Context) (Result[EmailChanges], error) { //NOSONAR
|
|
logger := j.loggerParams("GetEmailChanges", ctx, func(z zerolog.Context) zerolog.Context {
|
|
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, string(sinceState))
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
changes := EmailChangesCommand{
|
|
AccountId: accountId,
|
|
SinceState: sinceState,
|
|
}
|
|
if maxChanges > 0 {
|
|
changes.MaxChanges = &maxChanges
|
|
}
|
|
|
|
getCreated := EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
FetchAllBodyValues: fetchBodies,
|
|
IdsRef: &ResultReference{Name: CommandEmailChanges, Path: "/created", ResultOf: "0"},
|
|
}
|
|
if maxBodyValueBytes > 0 {
|
|
getCreated.MaxBodyValueBytes = maxBodyValueBytes
|
|
}
|
|
getUpdated := EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
FetchAllBodyValues: fetchBodies,
|
|
IdsRef: &ResultReference{Name: CommandEmailChanges, Path: "/updated", ResultOf: "0"},
|
|
}
|
|
if maxBodyValueBytes > 0 {
|
|
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL,
|
|
invocation(changes, "0"),
|
|
invocation(getCreated, "1"),
|
|
invocation(getUpdated, "2"),
|
|
)
|
|
if err != nil {
|
|
return ZeroResult[EmailChanges](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (EmailChanges, State, Error) {
|
|
var changesResponse EmailChangesResponse
|
|
err = retrieveChanges(ctx, body, changes, "0", &changesResponse)
|
|
if err != nil {
|
|
return EmailChanges{}, "", err
|
|
}
|
|
|
|
var createdResponse EmailGetResponse
|
|
err = retrieveGet(ctx, body, getCreated, "1", &createdResponse)
|
|
if err != nil {
|
|
logger.Error().Err(err).Send()
|
|
return EmailChanges{}, "", err
|
|
}
|
|
|
|
var updatedResponse EmailGetResponse
|
|
err = retrieveGet(ctx, body, getUpdated, "2", &updatedResponse)
|
|
if err != nil {
|
|
logger.Error().Err(err).Send()
|
|
return EmailChanges{}, "", err
|
|
}
|
|
|
|
return EmailChanges{
|
|
Destroyed: changesResponse.Destroyed,
|
|
HasMoreChanges: changesResponse.HasMoreChanges,
|
|
OldState: changesResponse.OldState,
|
|
NewState: changesResponse.NewState,
|
|
Created: createdResponse.List,
|
|
Updated: updatedResponse.List,
|
|
}, updatedResponse.State, nil
|
|
})
|
|
}
|
|
|
|
type SearchSnippetWithMeta struct {
|
|
ReceivedAt time.Time `json:"receivedAt,omitzero"`
|
|
EmailId string `json:"emailId,omitempty"`
|
|
SearchSnippet
|
|
}
|
|
|
|
type EmailSnippetSearchResults SearchResultsTemplate[SearchSnippetWithMeta]
|
|
|
|
func (j *Client) QueryEmailSnippets(accountIds []AccountId, //NOSONAR
|
|
filter EmailFilterElement, position int, anchor string, anchorOffset *int, limit *uint,
|
|
ctx Context) (Result[map[AccountId]EmailSnippetSearchResults], error) {
|
|
logger := j.loggerParams("QueryEmailSnippets", ctx, func(z zerolog.Context) zerolog.Context {
|
|
l := z.Int(logPosition, position)
|
|
if limit != nil {
|
|
l = l.Uint(logLimit, *limit)
|
|
}
|
|
return l
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
uniqueAccountIds := structs.Uniq(accountIds)
|
|
invocations := make([]Invocation, len(uniqueAccountIds)*3)
|
|
for i, accountId := range uniqueAccountIds {
|
|
query := EmailQueryCommand{
|
|
AccountId: accountId,
|
|
Filter: filter,
|
|
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
|
CollapseThreads: true,
|
|
CalculateTotal: true,
|
|
Position: position,
|
|
Anchor: anchor,
|
|
AnchorOffset: anchorOffset,
|
|
Limit: limit,
|
|
}
|
|
|
|
mails := EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
ResultOf: mcid(accountId, "0"),
|
|
Name: CommandEmailQuery,
|
|
Path: "/ids/*",
|
|
},
|
|
FetchAllBodyValues: false,
|
|
MaxBodyValueBytes: 0,
|
|
Properties: []string{EmailPropertyId, EmailPropertyReceivedAt, EmailPropertySentAt},
|
|
}
|
|
|
|
snippet := SearchSnippetGetRefCommand{
|
|
AccountId: accountId,
|
|
Filter: filter,
|
|
EmailIdRef: &ResultReference{
|
|
ResultOf: mcid(accountId, "0"),
|
|
Name: CommandEmailQuery,
|
|
Path: "/ids/*",
|
|
},
|
|
}
|
|
|
|
invocations[i*3+0] = invocation(query, mcid(accountId, "0"))
|
|
invocations[i*3+1] = invocation(mails, mcid(accountId, "1"))
|
|
invocations[i*3+2] = invocation(snippet, mcid(accountId, "2"))
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL, invocations...)
|
|
if err != nil {
|
|
return ZeroResult[map[AccountId]EmailSnippetSearchResults](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSnippetSearchResults, State, Error) {
|
|
results := make(map[AccountId]EmailSnippetSearchResults, len(uniqueAccountIds))
|
|
states := make(map[AccountId]State, len(uniqueAccountIds))
|
|
for _, accountId := range uniqueAccountIds {
|
|
var queryResponse EmailQueryResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var mailResponse EmailGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailGet, mcid(accountId, "1"), &mailResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var snippetResponse SearchSnippetGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, 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++
|
|
}
|
|
|
|
states[accountId] = queryResponse.QueryState
|
|
|
|
results[accountId] = EmailSnippetSearchResults{
|
|
Results: snippets,
|
|
CanCalculateChanges: ChangeCalculation(queryResponse.CanCalculateChanges),
|
|
Total: uintPtr(queryResponse.Total),
|
|
Limit: valueIf(queryResponse.Limit, limit != nil),
|
|
Position: valueIf(queryResponse.Position, anchor == ""),
|
|
}
|
|
}
|
|
return results, squashState(states), nil
|
|
})
|
|
}
|
|
|
|
/*
|
|
type EmailQueryResult struct {
|
|
Emails []Email `json:"emails"`
|
|
Total uint `json:"total"`
|
|
Limit uint `json:"limit,omitzero"`
|
|
Position uint `json:"position,omitzero"`
|
|
QueryState State `json:"queryState"`
|
|
}
|
|
*/
|
|
|
|
func (j *Client) QueryEmails(accountIds []AccountId,
|
|
filter EmailFilterElement, position int, limit uint, fetchBodies bool, maxBodyValueBytes uint,
|
|
ctx Context) (Result[map[AccountId]EmailSearchResults], error) { //NOSONAR
|
|
logger := j.loggerParams("QueryEmails", ctx, func(z zerolog.Context) zerolog.Context {
|
|
return z.Bool(logFetchBodies, fetchBodies)
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
uniqueAccountIds := structs.Uniq(accountIds)
|
|
invocations := make([]Invocation, len(uniqueAccountIds)*2)
|
|
for i, accountId := range uniqueAccountIds {
|
|
query := EmailQueryCommand{
|
|
AccountId: accountId,
|
|
Filter: filter,
|
|
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
|
CollapseThreads: true,
|
|
CalculateTotal: true,
|
|
}
|
|
if position > 0 {
|
|
query.Position = position
|
|
}
|
|
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(query, mcid(accountId, "0"))
|
|
invocations[i*2+1] = invocation(mails, mcid(accountId, "1"))
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL, invocations...)
|
|
if err != nil {
|
|
return ZeroResult[map[AccountId]EmailSearchResults](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSearchResults, State, Error) {
|
|
results := make(map[AccountId]EmailSearchResults, len(uniqueAccountIds))
|
|
queryStates := map[AccountId]State{}
|
|
for _, accountId := range uniqueAccountIds {
|
|
var queryResponse EmailQueryResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var emailsResponse EmailGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailGet, mcid(accountId, "1"), &emailsResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
results[accountId] = EmailSearchResults{
|
|
Results: emailsResponse.List,
|
|
Total: &queryResponse.Total, // no need to valufIf() here, we always request with calculateTotals==true
|
|
Limit: queryResponse.Limit,
|
|
Position: queryResponse.Position,
|
|
CanCalculateChanges: queryResponse.CanCalculateChanges,
|
|
}
|
|
queryStates[accountId] = queryResponse.QueryState
|
|
}
|
|
return results, squashState(queryStates), nil
|
|
})
|
|
}
|
|
|
|
type EmailWithSnippets struct {
|
|
Email Email `json:"email"`
|
|
Snippets []SearchSnippet `json:"snippets,omitempty"`
|
|
}
|
|
|
|
type EmailQueryWithSnippetsResult struct {
|
|
Results []EmailWithSnippets `json:"results"`
|
|
Total uint `json:"total"`
|
|
Position *uint `json:"position,omitempty"`
|
|
Limit *uint `json:"limit,omitempty"`
|
|
QueryState State `json:"queryState"`
|
|
}
|
|
|
|
func (j *Client) QueryEmailsWithSnippets(accountIds []AccountId, //NOSONAR
|
|
filter EmailFilterElement, position int, anchor string, anchorOffset *int, limit *uint, collapseThreads bool, calculateTotal bool, fetchBodies bool, maxBodyValueBytes uint,
|
|
ctx Context) (Result[map[AccountId]EmailQueryWithSnippetsResult], error) {
|
|
logger := j.loggerParams("QueryEmailsWithSnippets", ctx, func(z zerolog.Context) zerolog.Context {
|
|
return z.Bool(logFetchBodies, fetchBodies)
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
uniqueAccountIds := structs.Uniq(accountIds)
|
|
invocations := make([]Invocation, len(uniqueAccountIds)*3)
|
|
for i, accountId := range uniqueAccountIds {
|
|
query := EmailQueryCommand{
|
|
AccountId: accountId,
|
|
Filter: filter,
|
|
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
|
CollapseThreads: collapseThreads,
|
|
CalculateTotal: calculateTotal,
|
|
Position: position,
|
|
Anchor: anchor,
|
|
AnchorOffset: anchorOffset,
|
|
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(query, mcid(accountId, "0"))
|
|
invocations[i*3+1] = invocation(snippet, mcid(accountId, "1"))
|
|
invocations[i*3+2] = invocation(mails, mcid(accountId, "2"))
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL, invocations...)
|
|
if err != nil {
|
|
return ZeroResult[map[AccountId]EmailQueryWithSnippetsResult](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailQueryWithSnippetsResult, State, Error) {
|
|
result := make(map[AccountId]EmailQueryWithSnippetsResult, len(uniqueAccountIds))
|
|
for _, accountId := range uniqueAccountIds {
|
|
var queryResponse EmailQueryResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var snippetResponse SearchSnippetGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandSearchSnippetGet, mcid(accountId, "1"), &snippetResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var emailsResponse EmailGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, 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: valueIf(queryResponse.Position, anchor == ""),
|
|
QueryState: queryResponse.QueryState,
|
|
}
|
|
}
|
|
return result, squashStateFunc(result, func(r EmailQueryWithSnippetsResult) State { return r.QueryState }), nil
|
|
})
|
|
}
|
|
|
|
type UploadedEmail struct {
|
|
Id string `json:"id"`
|
|
Size int `json:"size"`
|
|
Type string `json:"type"`
|
|
Sha512 string `json:"sha:512"`
|
|
}
|
|
|
|
func (j *Client) ImportEmail(accountId AccountId, data []byte, ctx Context) (Result[UploadedEmail], error) {
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
|
|
upload := BlobUploadCommand{
|
|
AccountId: accountId,
|
|
Create: map[string]UploadObject{
|
|
"0": {
|
|
Data: []DataSourceObject{{
|
|
DataAsBase64: encoded,
|
|
}},
|
|
Type: EmailMimeType,
|
|
},
|
|
},
|
|
}
|
|
|
|
getHash := BlobGetRefCommand{
|
|
AccountId: accountId,
|
|
IdRef: &ResultReference{
|
|
ResultOf: "0",
|
|
Name: CommandBlobUpload,
|
|
Path: "/ids",
|
|
},
|
|
Properties: []string{BlobPropertyDigestSha512},
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL,
|
|
invocation(upload, "0"),
|
|
invocation(getHash, "1"),
|
|
)
|
|
if err != nil {
|
|
return ZeroResult[UploadedEmail](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (UploadedEmail, State, Error) {
|
|
var uploadResponse BlobUploadResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandBlobUpload, "0", &uploadResponse)
|
|
if err != nil {
|
|
return UploadedEmail{}, "", err
|
|
}
|
|
|
|
var getResponse BlobGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandBlobGet, "1", &getResponse)
|
|
if err != nil {
|
|
ctx.Logger.Error().Err(err).Send()
|
|
return UploadedEmail{}, "", err
|
|
}
|
|
|
|
if len(uploadResponse.Created) != 1 {
|
|
ctx.Logger.Error().Msgf("%T.Created has %v elements instead of 1", uploadResponse, len(uploadResponse.Created))
|
|
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
|
|
}
|
|
upload, ok := uploadResponse.Created["0"]
|
|
if !ok {
|
|
ctx.Logger.Error().Msgf("%T.Created has no element '0'", uploadResponse)
|
|
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
|
|
}
|
|
|
|
if len(getResponse.List) != 1 {
|
|
ctx.Logger.Error().Msgf("%T.List has %v elements instead of 1", getResponse, len(getResponse.List))
|
|
return UploadedEmail{}, "", jmapError(err, JmapErrorInvalidJmapResponsePayload)
|
|
}
|
|
get := getResponse.List[0]
|
|
|
|
return UploadedEmail{
|
|
Id: upload.Id,
|
|
Size: upload.Size,
|
|
Type: upload.Type,
|
|
Sha512: get.DigestSha512,
|
|
}, State(get.DigestSha256), nil
|
|
})
|
|
|
|
}
|
|
|
|
func (j *Client) CreateEmail(accountId AccountId, email EmailChange, replaceId string, ctx Context) (Result[*Email], error) {
|
|
set := EmailSetCommand{
|
|
AccountId: accountId,
|
|
Create: map[string]EmailChange{
|
|
"c": email,
|
|
},
|
|
}
|
|
if replaceId != "" {
|
|
set.Destroy = []string{replaceId}
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL,
|
|
invocation(set, "0"),
|
|
)
|
|
if err != nil {
|
|
return ZeroResult[*Email](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (*Email, State, Error) {
|
|
var setResponse EmailSetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailSet, "0", &setResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if len(setResponse.NotCreated) > 0 {
|
|
// error occured
|
|
// TODO(pbleser-oc) handle submission errors
|
|
}
|
|
|
|
setErr, notok := setResponse.NotCreated["c"]
|
|
if notok {
|
|
ctx.Logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr)
|
|
return nil, "", setErrorError(setErr, EmailType)
|
|
}
|
|
|
|
created, ok := setResponse.Created["c"]
|
|
if !ok {
|
|
berr := fmt.Errorf("failed to find %s in %s response", EmailType, string(CommandEmailSet))
|
|
ctx.Logger.Error().Err(berr)
|
|
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
|
|
}
|
|
|
|
return created, setResponse.NewState, nil
|
|
})
|
|
}
|
|
|
|
// The Email/set method encompasses:
|
|
// - Changing the keywords of an Email (e.g., unread/flagged status)
|
|
// - Adding/removing an Email to/from Mailboxes (moving a message)
|
|
// - Deleting Emails
|
|
//
|
|
// To create drafts, use the CreateEmail function instead.
|
|
//
|
|
// To delete mails, use the DeleteEmails function instead.
|
|
func (j *Client) UpdateEmails(accountId AccountId, updates map[string]PatchObject, ctx Context) (Result[map[string]*Email], error) {
|
|
set := EmailSetCommand{
|
|
AccountId: accountId,
|
|
Update: updates,
|
|
}
|
|
cmd, err := j.request(ctx, NS_MAIL, invocation(set, "0"))
|
|
if err != nil {
|
|
return ZeroResult[map[string]*Email](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (map[string]*Email, State, Error) {
|
|
var setResponse EmailSetResponse
|
|
err = retrieveSet(ctx, body, set, "0", &setResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(setResponse.NotUpdated) > 0 {
|
|
// TODO we don't have composite errors
|
|
for _, notUpdated := range setResponse.NotUpdated {
|
|
return nil, "", setErrorError(notUpdated, EmailType)
|
|
}
|
|
}
|
|
return setResponse.Updated, setResponse.NewState, nil
|
|
})
|
|
}
|
|
|
|
func (j *Client) UpdateEmail(accountId AccountId, id string, changes EmailChange, ctx Context) (Result[Email], error) {
|
|
return update(j, "UpdateEmail", EmailType,
|
|
func(update map[string]PatchObject) EmailSetCommand {
|
|
return EmailSetCommand{AccountId: accountId, Update: update}
|
|
},
|
|
func(id string) EmailGetCommand {
|
|
return EmailGetCommand{AccountId: accountId, Ids: []string{id}}
|
|
},
|
|
func(resp EmailSetResponse) map[string]SetError { return resp.NotUpdated },
|
|
func(resp EmailGetResponse) Email { return resp.List[0] },
|
|
id, changes,
|
|
ctx,
|
|
)
|
|
}
|
|
|
|
func (j *Client) DeleteEmails(accountId AccountId, destroyIds []string, ctx Context) (Result[map[string]SetError], error) {
|
|
return destroy(j, "DeleteEmails", EmailType,
|
|
func(accountId AccountId, destroy []string) EmailSetCommand {
|
|
return EmailSetCommand{AccountId: accountId, Destroy: destroy}
|
|
},
|
|
EmailSetResponse{},
|
|
accountId, destroyIds,
|
|
ctx,
|
|
)
|
|
}
|
|
|
|
type SubmittedEmail struct {
|
|
Id string `json:"id"`
|
|
SendAt time.Time `json:"sendAt,omitzero"`
|
|
ThreadId string `json:"threadId,omitempty"`
|
|
UndoStatus EmailSubmissionUndoStatus `json:"undoStatus,omitempty"`
|
|
Envelope *Envelope `json:"envelope,omitempty"`
|
|
|
|
// A list of blob ids for DSNs [RFC3464] received for this submission,
|
|
// in order of receipt, oldest first.
|
|
//
|
|
// The blob is the whole MIME message (with a top-level content-type of multipart/report), as received.
|
|
//
|
|
// [RFC3464]: https://datatracker.ietf.org/doc/html/rfc3464
|
|
DsnBlobIds []string `json:"dsnBlobIds,omitempty"`
|
|
|
|
// A list of blob ids for MDNs [RFC8098] received for this submission,
|
|
// in order of receipt, oldest first.
|
|
//
|
|
// The blob is the whole MIME message (with a top-level content-type of multipart/report), as received.
|
|
//
|
|
// [RFC8098]: https://datatracker.ietf.org/doc/html/rfc8098
|
|
MdnBlobIds []string `json:"mdnBlobIds,omitempty"`
|
|
}
|
|
|
|
type MoveMail struct {
|
|
FromMailboxId string
|
|
ToMailboxId string
|
|
}
|
|
|
|
func (j *Client) SubmitEmail(accountId AccountId, identityId string, emailId string, move *MoveMail, //NOSONAR
|
|
ctx Context) (Result[EmailSubmission], error) {
|
|
logger := j.logger("SubmitEmail", ctx)
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
update := map[string]any{
|
|
EmailPropertyKeywords + "/" + JmapKeywordDraft: nil, // unmark as draft
|
|
EmailPropertyKeywords + "/" + JmapKeywordSeen: true, // mark as seen (read)
|
|
}
|
|
if move != nil && move.FromMailboxId != "" && move.ToMailboxId != "" && move.FromMailboxId != move.ToMailboxId {
|
|
update[EmailPropertyMailboxIds+"/"+move.FromMailboxId] = nil
|
|
update[EmailPropertyMailboxIds+"/"+move.ToMailboxId] = true
|
|
}
|
|
|
|
id := "s0"
|
|
|
|
submit := EmailSubmissionSetCommand{
|
|
AccountId: accountId,
|
|
Create: map[string]EmailSubmissionCreate{
|
|
id: {
|
|
IdentityId: identityId,
|
|
EmailId: emailId,
|
|
Envelope: nil,
|
|
},
|
|
},
|
|
OnSuccessUpdateEmail: map[string]PatchObject{
|
|
"#" + id: update,
|
|
},
|
|
}
|
|
|
|
get := EmailSubmissionGetCommand{
|
|
AccountId: accountId,
|
|
Ids: []string{"#" + id},
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL_SUBMISSION,
|
|
invocation(submit, "0"),
|
|
invocation(get, "1"),
|
|
)
|
|
if err != nil {
|
|
return ZeroResult[EmailSubmission](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (EmailSubmission, State, Error) {
|
|
var submissionResponse EmailSubmissionSetResponse
|
|
err = retrieveSet(ctx, body, submit, "0", &submissionResponse)
|
|
if err != nil {
|
|
return EmailSubmission{}, "", err
|
|
}
|
|
|
|
if len(submissionResponse.NotCreated) > 0 {
|
|
// error occured
|
|
// TODO(pbleser-oc) handle submission errors
|
|
}
|
|
|
|
// there is an implicit Email/set response:
|
|
// "After all create/update/destroy items in the EmailSubmission/set invocation have been processed,
|
|
// a single implicit Email/set call MUST be made to perform any changes requested in these two arguments.
|
|
// The response to this MUST be returned after the EmailSubmission/set response."
|
|
// from an example in the spec, it has the same tag as the EmailSubmission/set command ("0" in this case)
|
|
var setResponse EmailSetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailSet, "0", &setResponse)
|
|
if err != nil {
|
|
return EmailSubmission{}, "", err
|
|
}
|
|
|
|
if len(setResponse.Updated) == 1 {
|
|
var getResponse EmailSubmissionGetResponse
|
|
err = retrieveGet(ctx, body, get, "1", &getResponse)
|
|
if err != nil {
|
|
return EmailSubmission{}, "", err
|
|
}
|
|
|
|
if len(getResponse.List) != 1 {
|
|
// for some reason (error?)...
|
|
// TODO(pbleser-oc) handle absence of emailsubmission
|
|
}
|
|
|
|
submission := getResponse.List[0]
|
|
|
|
return submission, setResponse.NewState, nil
|
|
} else {
|
|
err = jmapError(fmt.Errorf("failed to submit email: updated is empty"), 0) // TODO proper error handling
|
|
return EmailSubmission{}, "", err
|
|
}
|
|
})
|
|
}
|
|
|
|
func (j *Client) GetEmailSubmissionStatus(accountId AccountId, submissionIds []string, ctx Context) (Result[EmailSubmissionGetResponse], error) {
|
|
logger := j.logger("GetEmailSubmissionStatus", ctx)
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
get := EmailSubmissionGetCommand{
|
|
AccountId: accountId,
|
|
Ids: submissionIds,
|
|
}
|
|
cmd, err := j.request(ctx, NS_MAIL_SUBMISSION, invocation(get, "0"))
|
|
if err != nil {
|
|
return ZeroResult[EmailSubmissionGetResponse](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (EmailSubmissionGetResponse, State, Error) {
|
|
var response EmailSubmissionGetResponse
|
|
err = retrieveGet(ctx, body, get, "0", &response)
|
|
if err != nil {
|
|
return EmailSubmissionGetResponse{}, "", err
|
|
}
|
|
return response, response.State, nil
|
|
})
|
|
}
|
|
|
|
func (j *Client) EmailsInThread(accountId AccountId, threadId string,
|
|
fetchBodies bool, maxBodyValueBytes uint,
|
|
ctx Context) (Result[[]Email], Error) { //NOSONAR
|
|
logger := j.loggerParams("EmailsInThread", ctx, func(z zerolog.Context) zerolog.Context {
|
|
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
|
|
})
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
thread := ThreadGetCommand{
|
|
AccountId: accountId,
|
|
Ids: []string{threadId},
|
|
}
|
|
|
|
get := EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
ResultOf: "0",
|
|
Name: CommandThreadGet,
|
|
Path: "/list/*/emailIds",
|
|
},
|
|
FetchAllBodyValues: fetchBodies,
|
|
MaxBodyValueBytes: maxBodyValueBytes,
|
|
}
|
|
|
|
cmd, err := j.request(ctx, NS_MAIL,
|
|
invocation(thread, "0"),
|
|
invocation(get, "1"),
|
|
)
|
|
if err != nil {
|
|
return ZeroResult[[]Email](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) ([]Email, State, Error) {
|
|
var emailsResponse EmailGetResponse
|
|
err = retrieveGet(ctx, body, get, "1", &emailsResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return emailsResponse.List, emailsResponse.State, nil
|
|
})
|
|
}
|
|
|
|
type EmailsSummary struct {
|
|
Emails []Email `json:"emails"`
|
|
Total uint `json:"total"`
|
|
Limit *uint `json:"limit,omitempty"`
|
|
Position *uint `json:"position,omitempty"`
|
|
State State `json:"state"`
|
|
}
|
|
|
|
var EmailSummaryProperties = []string{
|
|
EmailPropertyId,
|
|
EmailPropertyThreadId,
|
|
EmailPropertyMailboxIds,
|
|
EmailPropertyKeywords,
|
|
EmailPropertySize,
|
|
EmailPropertyReceivedAt,
|
|
EmailPropertySender,
|
|
EmailPropertyFrom,
|
|
EmailPropertyTo,
|
|
EmailPropertyCc,
|
|
EmailPropertyBcc,
|
|
EmailPropertySubject,
|
|
EmailPropertySentAt,
|
|
EmailPropertyHasAttachment,
|
|
EmailPropertyAttachments,
|
|
EmailPropertyPreview,
|
|
}
|
|
|
|
func (j *Client) QueryEmailSummaries(accountIds []AccountId, //NOSONAR
|
|
filter EmailFilterElement, position int, anchor string, anchorOffset *int, limit *uint, withThreads bool, calculateTotal bool,
|
|
ctx Context) (Result[map[AccountId]EmailsSummary], error) {
|
|
logger := j.logger("QueryEmailSummaries", ctx)
|
|
ctx = ctx.WithLogger(logger)
|
|
|
|
uniqueAccountIds := structs.Uniq(accountIds)
|
|
|
|
factor := 2
|
|
if withThreads {
|
|
factor++
|
|
}
|
|
|
|
invocations := make([]Invocation, len(uniqueAccountIds)*factor)
|
|
for i, accountId := range uniqueAccountIds {
|
|
get := EmailQueryCommand{
|
|
AccountId: accountId,
|
|
Filter: filter,
|
|
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
|
CalculateTotal: calculateTotal,
|
|
Position: position,
|
|
Anchor: anchor,
|
|
AnchorOffset: anchorOffset,
|
|
Limit: limit,
|
|
}
|
|
invocations[i*factor+0] = invocation(&get, mcid(accountId, "0"))
|
|
|
|
invocations[i*factor+1] = invocation(EmailGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
Name: CommandEmailQuery,
|
|
Path: "/ids/*",
|
|
ResultOf: mcid(accountId, "0"),
|
|
},
|
|
Properties: EmailSummaryProperties,
|
|
}, mcid(accountId, "1"))
|
|
if withThreads {
|
|
invocations[i*factor+2] = invocation(ThreadGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
Name: CommandEmailGet,
|
|
Path: "/list/*/" + EmailPropertyThreadId,
|
|
ResultOf: mcid(accountId, "1"),
|
|
},
|
|
}, mcid(accountId, "2"))
|
|
}
|
|
}
|
|
cmd, err := j.request(ctx, NS_MAIL, invocations...)
|
|
if err != nil {
|
|
return ZeroResult[map[AccountId]EmailsSummary](), err
|
|
}
|
|
|
|
return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailsSummary, State, Error) {
|
|
resp := map[AccountId]EmailsSummary{}
|
|
for _, accountId := range uniqueAccountIds {
|
|
var queryResponse EmailQueryResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailQuery, mcid(accountId, "0"), &queryResponse)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var response EmailGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandEmailGet, mcid(accountId, "1"), &response)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(response.NotFound) > 0 {
|
|
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
|
|
}
|
|
if withThreads {
|
|
var thread ThreadGetResponse
|
|
err = retrieveResponseMatchParameters(ctx, body, CommandThreadGet, mcid(accountId, "2"), &thread)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
setThreadSize(&thread, response.List)
|
|
}
|
|
|
|
resp[accountId] = EmailsSummary{
|
|
Emails: response.List,
|
|
Total: queryResponse.Total,
|
|
Limit: queryResponse.Limit,
|
|
Position: valueIf(queryResponse.Position, anchor == ""),
|
|
State: response.State,
|
|
}
|
|
}
|
|
return resp, squashStateFunc(resp, func(s EmailsSummary) State { return s.State }), nil
|
|
})
|
|
}
|
|
|
|
type EmailSubmissionChanges = ChangesTemplate[EmailSubmission]
|
|
|
|
// Retrieve the changes in Email Submissions since a given State.
|
|
// @api:tags email,changes
|
|
func (j *Client) GetEmailSubmissionChanges(accountId AccountId, sinceState State, maxChanges uint,
|
|
ctx Context) (Result[EmailSubmissionChanges], error) {
|
|
return changes(j, "GetEmailSubmissionChanges", EmailSubmissionType,
|
|
func() EmailSubmissionChangesCommand {
|
|
return EmailSubmissionChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: uintPtr(maxChanges)}
|
|
},
|
|
EmailSubmissionChangesResponse{},
|
|
func(path string, rof string) EmailSubmissionGetRefCommand {
|
|
return EmailSubmissionGetRefCommand{
|
|
AccountId: accountId,
|
|
IdsRef: &ResultReference{
|
|
Name: CommandEmailSubmissionChanges,
|
|
Path: path,
|
|
ResultOf: rof,
|
|
},
|
|
}
|
|
},
|
|
func(resp EmailSubmissionGetResponse) []EmailSubmission { return resp.List },
|
|
func(oldState, newState State, hasMoreChanges bool, created, updated []EmailSubmission, destroyed []string) EmailSubmissionChanges {
|
|
return EmailSubmissionChanges{
|
|
OldState: oldState,
|
|
NewState: newState,
|
|
HasMoreChanges: hasMoreChanges,
|
|
Created: created,
|
|
Updated: updated,
|
|
Destroyed: destroyed,
|
|
}
|
|
},
|
|
ctx,
|
|
)
|
|
}
|
|
|
|
func setThreadSize(threads *ThreadGetResponse, emails []Email) {
|
|
threadSizeById := make(map[string]int, len(threads.List))
|
|
for _, thread := range threads.List {
|
|
threadSizeById[thread.Id] = len(thread.EmailIds)
|
|
}
|
|
for i := range len(emails) {
|
|
ts, ok := threadSizeById[emails[i].ThreadId]
|
|
if !ok {
|
|
ts = 1
|
|
}
|
|
emails[i].ThreadSize = ts
|
|
}
|
|
}
|