groupware: add retrieving and adding mailboxIds for drafts and sent if they are missing

This commit is contained in:
Pascal Bleser
2025-12-05 10:36:31 +01:00
parent 196ee7b3e4
commit ddbfef3ce3
7 changed files with 276 additions and 34 deletions

View File

@@ -18,8 +18,13 @@ type Emails struct {
Offset uint `json:"offset,omitzero"`
}
type getEmailsResult struct {
emails []Email
notFound []string
}
// Retrieve specific Emails by their id.
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) ([]Email, SessionState, State, Language, Error) {
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) ([]Email, []string, SessionState, State, Language, Error) {
logger = j.logger("GetEmails", session, logger)
get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies}
@@ -52,35 +57,36 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
cmd, err := j.request(session, logger, methodCalls...)
if err != nil {
logger.Error().Err(err).Send()
return nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return nil, nil, "", "", "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]Email, State, Error) {
result, sessionState, state, language, gwerr := command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (getEmailsResult, State, Error) {
if markAsSeen {
var markResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &markResponse)
if err != nil {
return nil, "", err
return getEmailsResult{}, "", err
}
for _, seterr := range markResponse.NotUpdated {
// TODO we don't have a way to compose multiple set errors yet
return nil, "", setErrorError(seterr, EmailType)
return getEmailsResult{}, "", setErrorError(seterr, EmailType)
}
}
var response EmailGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, "1", &response)
if err != nil {
return nil, "", err
return getEmailsResult{}, "", err
}
if withThreads {
var threads ThreadGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandThreadGet, "2", &threads)
if err != nil {
return nil, "", err
return getEmailsResult{}, "", err
}
setThreadSize(&threads, response.List)
}
return response.List, response.State, nil
return getEmailsResult{emails: response.List, notFound: response.NotFound}, response.State, nil
})
return result.emails, result.notFound, sessionState, state, language, gwerr
}
func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string) (string, SessionState, State, Language, Error) {
@@ -890,7 +896,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
return EmailSubmission{}, "", err
}
if emailId := structs.FirstKey(setResponse.Updated); emailId != nil && len(setResponse.Updated) == 1 {
if len(setResponse.Updated) == 1 {
var getResponse EmailSubmissionGetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "1", &getResponse)
if err != nil {

View File

@@ -114,6 +114,46 @@ func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx cont
})
}
func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, roles []string) (map[string]map[string]string, SessionState, State, Language, Error) {
logger = j.logger("SearchMailboxIdsPerRole", session, logger)
uniqueAccountIds := structs.Uniq(accountIds)
invocations := make([]Invocation, len(uniqueAccountIds)*len(roles))
for i, accountId := range uniqueAccountIds {
for j, role := range roles {
invocations[i*len(roles)+j] = invocation(CommandMailboxQuery, MailboxQueryCommand{AccountId: accountId, Filter: MailboxFilterCondition{Role: role}}, mcid(accountId, role))
}
}
cmd, err := j.request(session, logger, invocations...)
if err != nil {
return nil, "", "", "", err
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]map[string]string, State, Error) {
resp := map[string]map[string]string{}
stateByAccountid := map[string]State{}
for _, accountId := range uniqueAccountIds {
mailboxIdsByRole := map[string]string{}
for _, role := range roles {
var response MailboxQueryResponse
err = retrieveResponseMatchParameters(logger, body, CommandMailboxQuery, mcid(accountId, role), &response)
if err != nil {
return nil, "", err
}
if len(response.Ids) == 1 {
mailboxIdsByRole[role] = response.Ids[0]
}
if _, ok := stateByAccountid[accountId]; !ok {
stateByAccountid[accountId] = response.QueryState
}
}
resp[accountId] = mailboxIdsByRole
}
return resp, squashState(stateByAccountid), nil
})
}
type MailboxChanges struct {
Destroyed []string `json:"destroyed,omitzero"`
HasMoreChanges bool `json:"hasMoreChanges,omitzero"`

View File

@@ -134,6 +134,106 @@ func TestEmails(t *testing.T) {
}
}
func TestSendingEmails(t *testing.T) {
if skip(t) {
return
}
require := require.New(t)
s, err := newStalwartTest(t)
require.NoError(err)
defer s.Close()
accountId := s.session.PrimaryAccounts.Mail
var mailboxPerRole map[string]Mailbox
{
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
require.NoError(err)
mailboxPerRole = structs.Index(mailboxes[accountId], func(m Mailbox) string { return m.Role })
require.Contains(mailboxPerRole, JmapMailboxRoleInbox)
require.Contains(mailboxPerRole, JmapMailboxRoleDrafts)
require.Contains(mailboxPerRole, JmapMailboxRoleSent)
require.Contains(mailboxPerRole, JmapMailboxRoleTrash)
}
{
roles := []string{JmapMailboxRoleDrafts, JmapMailboxRoleSent, JmapMailboxRoleInbox}
m, _, _, _, err := s.client.SearchMailboxIdsPerRole([]string{accountId}, s.session, s.ctx, s.logger, "", roles)
require.NoError(err)
require.Contains(m, accountId)
a := m[accountId]
for _, role := range roles {
require.Contains(a, role)
}
}
{
var identity Identity
{
identities, _, _, _, err := s.client.GetAllIdentities(accountId, s.session, s.ctx, s.logger, "")
require.NoError(err)
require.NotEmpty(identities)
identity = identities[0]
}
create := EmailCreate{
Keywords: toBoolMapS("test"),
Subject: "testing 123",
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
}
created, _, _, _, err := s.client.CreateEmail(accountId, create, "", s.session, s.ctx, s.logger, "")
require.NoError(err)
require.NotEmpty(created.Id)
{
emails, notFound, _, _, _, err := s.client.GetEmails(accountId, s.session, s.ctx, s.logger, "", []string{created.Id}, true, 0, false, false)
require.NoError(err)
require.Len(emails, 1)
require.Empty(notFound)
email := emails[0]
require.Equal(created.Id, email.Id)
require.Len(email.MailboxIds, 1)
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
}
update := EmailCreate{
To: []EmailAddress{{Name: identity.Name, Email: identity.Email}},
Keywords: toBoolMapS("test"),
Subject: "testing 1234",
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
}
updated, _, _, _, err := s.client.CreateEmail(accountId, update, created.Id, s.session, s.ctx, s.logger, "")
require.NoError(err)
require.NotEmpty(updated.Id)
require.NotEqual(created.Id, updated.Id)
var updatedMailboxId string
{
emails, notFound, _, _, _, err := s.client.GetEmails(accountId, s.session, s.ctx, s.logger, "", []string{created.Id, updated.Id}, true, 0, false, false)
require.NoError(err)
require.Len(emails, 1)
require.Len(notFound, 1)
email := emails[0]
require.Equal(updated.Id, email.Id)
require.Len(email.MailboxIds, 1)
require.Contains(email.MailboxIds, mailboxPerRole[JmapMailboxRoleDrafts].Id)
require.Equal(notFound[0], created.Id)
var ok bool
updatedMailboxId, ok = structs.FirstKey(email.MailboxIds)
require.True(ok)
}
move := MoveMail{
FromMailboxId: updatedMailboxId,
ToMailboxId: mailboxPerRole[JmapMailboxRoleSent].Id,
}
sub, _, _, _, err := s.client.SubmitEmail(accountId, identity.Id, updated.Id, &move, s.session, s.ctx, s.logger, "")
fmt.Printf("sub: %v\n", sub)
}
}
func matchEmail(t *testing.T, actual Email, expected filledMail, hasBodies bool) {
require := require.New(t)
require.Len(actual.MessageId, 1)

View File

@@ -1390,7 +1390,6 @@ type MailboxFilterElement interface {
}
type MailboxFilterCondition struct {
MailboxFilterElement
ParentId string `json:"parentId,omitempty"`
Name string `json:"name,omitempty"`
Role string `json:"role,omitempty"`
@@ -1398,14 +1397,17 @@ type MailboxFilterCondition struct {
IsSubscribed *bool `json:"isSubscribed,omitempty"`
}
func (c MailboxFilterCondition) _isAMailboxFilterElement() {}
var _ MailboxFilterElement = &MailboxFilterCondition{}
type MailboxFilterOperator struct {
MailboxFilterElement
Operator FilterOperatorTerm `json:"operator"`
Conditions []MailboxFilterElement `json:"conditions,omitempty"`
}
func (c MailboxFilterOperator) _isAMailboxFilterElement() {}
var _ MailboxFilterElement = &MailboxFilterOperator{}
type MailboxComparator struct {
@@ -2840,7 +2842,7 @@ type EmailGetResponse struct {
// This array contains the ids passed to the method for records that do not exist.
//
// The array is empty if all requested ids were found or if the ids argument passed in was either null or an empty array.
NotFound []any `json:"notFound"`
NotFound []string `json:"notFound"`
}
type EmailChangesResponse struct {

View File

@@ -170,11 +170,12 @@ func Missing[E comparable](expected, actual []E) []E {
return missing
}
func FirstKey[K comparable, V any](m map[K]V) *K {
func FirstKey[K comparable, V any](m map[K]V) (K, bool) {
for k := range m {
return &k
return k, true
}
return nil
var zero K
return zero, false
}
func Any[E any](s []E, predicate func(E) bool) bool {