mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-06 04:11:21 -05:00
groupware: improve email submission and testing
* jmap/EmailCreate: add more attributes that were omitted: Headers, InReplyTo, References, Sender * add jmap GetEmailSubmissionStatus * improve email integration tests by adding a thorough test for email submission * jmap integration tests: provision principals and domains using the Stalwart Management API, switching from an in-memory to an internal directory
This commit is contained in:
@@ -40,12 +40,10 @@ type BlobClient interface {
|
||||
|
||||
const (
|
||||
logOperation = "operation"
|
||||
logMailboxId = "mailbox-id"
|
||||
logFetchBodies = "fetch-bodies"
|
||||
logOffset = "offset"
|
||||
logLimit = "limit"
|
||||
logDownloadUrl = "download-url"
|
||||
logBlobId = "blob-id"
|
||||
logUploadUrl = "download-url"
|
||||
logSinceState = "since-state"
|
||||
)
|
||||
|
||||
@@ -845,7 +845,7 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
id: {
|
||||
IdentityId: identityId,
|
||||
EmailId: emailId,
|
||||
// leaving Envelope empty
|
||||
Envelope: nil,
|
||||
},
|
||||
},
|
||||
OnSuccessUpdateEmail: map[string]PatchObject{
|
||||
@@ -856,13 +856,6 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
get := EmailSubmissionGetCommand{
|
||||
AccountId: accountId,
|
||||
Ids: []string{"#" + id},
|
||||
/*
|
||||
IdRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandEmailSubmissionSet,
|
||||
Path: ["#"]"/created/" + "#" + id + "/" + EmailPropertyId,
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
cmd, err := j.request(session, logger,
|
||||
@@ -918,6 +911,38 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
|
||||
})
|
||||
}
|
||||
|
||||
type emailSubmissionResult struct {
|
||||
submissions map[string]EmailSubmission
|
||||
notFound []string
|
||||
}
|
||||
|
||||
func (j *Client) GetEmailSubmissionStatus(accountId string, submissionIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]EmailSubmission, []string, SessionState, State, Language, Error) {
|
||||
logger = j.logger("GetEmailSubmissionStatus", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger, invocation(CommandEmailSubmissionGet, EmailSubmissionGetCommand{
|
||||
AccountId: accountId,
|
||||
Ids: submissionIds,
|
||||
}, "0"))
|
||||
if err != nil {
|
||||
return nil, nil, "", "", "", err
|
||||
}
|
||||
|
||||
result, sessionState, state, lang, err := command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (emailSubmissionResult, State, Error) {
|
||||
var response EmailSubmissionGetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "0", &response)
|
||||
if err != nil {
|
||||
return emailSubmissionResult{}, "", err
|
||||
}
|
||||
m := make(map[string]EmailSubmission, len(response.List))
|
||||
for _, s := range response.List {
|
||||
m[s.Id] = s
|
||||
}
|
||||
return emailSubmissionResult{submissions: m, notFound: response.NotFound}, response.State, nil
|
||||
})
|
||||
|
||||
return result.submissions, result.notFound, sessionState, state, lang, err
|
||||
}
|
||||
|
||||
func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, State, Language, Error) {
|
||||
logger = j.loggerParams("EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context {
|
||||
return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId))
|
||||
|
||||
@@ -39,7 +39,10 @@ func TestContacts(t *testing.T) {
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count)
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count, session, user)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(addressbookId)
|
||||
@@ -51,7 +54,7 @@ func TestContacts(t *testing.T) {
|
||||
{Property: jscontact.ContactCardPropertyCreated, IsAscending: true},
|
||||
}
|
||||
|
||||
contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, s.session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
|
||||
contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(contactsByAccount, 1)
|
||||
@@ -97,9 +100,11 @@ var streetNumberRegex = regexp.MustCompile(`^(\d+)\s+(.+)$`)
|
||||
func (s *StalwartTest) fillContacts(
|
||||
t *testing.T,
|
||||
count uint,
|
||||
session *Session,
|
||||
user User,
|
||||
) (string, string, map[string]jscontact.ContactCard, ContactsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(s.session, s.username, s.password, true, true)
|
||||
c, err := NewTestJmapClient(session, user.name, user.password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
|
||||
@@ -25,34 +25,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func (s *StalwartTest) findInbox(t *testing.T, accountId string) (string, string) {
|
||||
require := require.New(t)
|
||||
respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Len(respByAccountId, 1)
|
||||
require.Contains(respByAccountId, accountId)
|
||||
resp := respByAccountId[accountId]
|
||||
|
||||
mailboxesNameByRole := map[string]string{}
|
||||
mailboxesUnreadByRole := map[string]int{}
|
||||
for _, m := range resp {
|
||||
if m.Role != "" {
|
||||
mailboxesNameByRole[m.Role] = m.Name
|
||||
mailboxesUnreadByRole[m.Role] = m.UnreadEmails
|
||||
}
|
||||
}
|
||||
require.Contains(mailboxesNameByRole, "inbox")
|
||||
require.Contains(mailboxesUnreadByRole, "inbox")
|
||||
require.Zero(mailboxesUnreadByRole["inbox"])
|
||||
|
||||
inboxId := mailboxId("inbox", resp)
|
||||
require.NotEmpty(inboxId)
|
||||
inboxFolder := mailboxesNameByRole["inbox"]
|
||||
require.NotEmpty(inboxFolder)
|
||||
return inboxId, inboxFolder
|
||||
}
|
||||
|
||||
func TestEmails(t *testing.T) {
|
||||
if skip(t) {
|
||||
return
|
||||
@@ -66,32 +38,35 @@ func TestEmails(t *testing.T) {
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
accountId := s.session.PrimaryAccounts.Mail
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
inboxId, inboxFolder := s.findInbox(t, accountId)
|
||||
accountId := session.PrimaryAccounts.Mail
|
||||
|
||||
inboxId, inboxFolder := s.findInbox(t, accountId, session)
|
||||
|
||||
var threads int = 0
|
||||
var mails []filledMail = nil
|
||||
{
|
||||
mails, threads, err = s.fillEmailsWithImap(inboxFolder, count, false)
|
||||
mails, threads, err = s.fillEmailsWithImap(inboxFolder, count, false, user)
|
||||
require.NoError(err)
|
||||
}
|
||||
mailsByMessageId := structs.Index(mails, func(mail filledMail) string { return mail.messageId })
|
||||
|
||||
{
|
||||
{
|
||||
resp, sessionState, _, _, err := s.client.GetAllIdentities(accountId, s.session, s.ctx, s.logger, "")
|
||||
resp, sessionState, _, _, err := s.client.GetAllIdentities(accountId, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.Len(resp, 1)
|
||||
require.Equal(s.userEmail, resp[0].Email)
|
||||
require.Equal(s.userPersonName, resp[0].Name)
|
||||
require.Equal(user.email, resp[0].Email)
|
||||
require.Equal(user.description, resp[0].Name)
|
||||
}
|
||||
|
||||
{
|
||||
respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
|
||||
respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.Len(respByAccountId, 1)
|
||||
require.Contains(respByAccountId, accountId)
|
||||
resp := respByAccountId[accountId]
|
||||
@@ -105,9 +80,9 @@ func TestEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, true, false, 0, true)
|
||||
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, session, s.ctx, s.logger, "", inboxId, 0, 0, true, false, 0, true)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
|
||||
require.Equalf(threads, len(resp.Emails), "the number of collapsed emails in the inbox is expected to be %v, but is actually %v", threads, len(resp.Emails))
|
||||
for _, e := range resp.Emails {
|
||||
@@ -119,9 +94,9 @@ func TestEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, s.session, s.ctx, s.logger, "", inboxId, 0, 0, false, false, 0, true)
|
||||
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, session, s.ctx, s.logger, "", inboxId, 0, 0, false, false, 0, true)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
|
||||
require.Equalf(count, len(resp.Emails), "the number of emails in the inbox is expected to be %v, but is actually %v", count, len(resp.Emails))
|
||||
for _, e := range resp.Emails {
|
||||
@@ -145,11 +120,29 @@ func TestSendingEmails(t *testing.T) {
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
accountId := s.session.PrimaryAccounts.Mail
|
||||
from := pickUser()
|
||||
session := s.Session(from.name)
|
||||
accountId := session.PrimaryAccounts.Mail
|
||||
|
||||
var to User
|
||||
{
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.name != from.name })
|
||||
to = others[rand.Intn(len(others))]
|
||||
}
|
||||
toSession := s.Session(to.name)
|
||||
toAccountId := toSession.PrimaryAccounts.Mail
|
||||
|
||||
var cc User
|
||||
{
|
||||
others := structs.Filter(users[:], func(u User) bool { return u.name != from.name && u.name != to.name })
|
||||
cc = others[rand.Intn(len(others))]
|
||||
}
|
||||
ccSession := s.Session(cc.name)
|
||||
ccAccountId := ccSession.PrimaryAccounts.Mail
|
||||
|
||||
var mailboxPerRole map[string]Mailbox
|
||||
{
|
||||
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{accountId}, s.session, s.ctx, s.logger, "")
|
||||
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{accountId}, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
mailboxPerRole = structs.Index(mailboxes[accountId], func(m Mailbox) string { return m.Role })
|
||||
require.Contains(mailboxPerRole, JmapMailboxRoleInbox)
|
||||
@@ -159,7 +152,7 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
{
|
||||
roles := []string{JmapMailboxRoleDrafts, JmapMailboxRoleSent, JmapMailboxRoleInbox}
|
||||
m, _, _, _, err := s.client.SearchMailboxIdsPerRole([]string{accountId}, s.session, s.ctx, s.logger, "", roles)
|
||||
m, _, _, _, err := s.client.SearchMailboxIdsPerRole([]string{accountId}, session, s.ctx, s.logger, "", roles)
|
||||
require.NoError(err)
|
||||
require.Contains(m, accountId)
|
||||
a := m[accountId]
|
||||
@@ -168,10 +161,26 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// let's ensure that the recipients have zero emails in their mailboxes before we send them any
|
||||
for _, u := range []struct {
|
||||
accountId string
|
||||
session *Session
|
||||
}{{toAccountId, toSession}, {ccAccountId, ccSession}} {
|
||||
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{u.accountId}, u.session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
for _, mailbox := range mailboxes[u.accountId] {
|
||||
require.Equal(0, mailbox.TotalEmails)
|
||||
}
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Test Subject %d", 10000+rand.Intn(90000))
|
||||
fromName := fmt.Sprintf("%s (test %d)", from.name, 1000+rand.Intn(9000))
|
||||
sender := EmailAddress{Email: from.email, Name: from.description}
|
||||
|
||||
{
|
||||
var identity Identity
|
||||
{
|
||||
identities, _, _, _, err := s.client.GetAllIdentities(accountId, s.session, s.ctx, s.logger, "")
|
||||
identities, _, _, _, err := s.client.GetAllIdentities(accountId, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.NotEmpty(identities)
|
||||
identity = identities[0]
|
||||
@@ -179,15 +188,15 @@ func TestSendingEmails(t *testing.T) {
|
||||
|
||||
create := EmailCreate{
|
||||
Keywords: toBoolMapS("test"),
|
||||
Subject: "testing 123",
|
||||
Subject: subject,
|
||||
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
|
||||
}
|
||||
created, _, _, _, err := s.client.CreateEmail(accountId, create, "", s.session, s.ctx, s.logger, "")
|
||||
created, _, _, _, err := s.client.CreateEmail(accountId, create, "", 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)
|
||||
emails, notFound, _, _, _, err := s.client.GetEmails(accountId, session, s.ctx, s.logger, "", []string{created.Id}, true, 0, false, false)
|
||||
require.NoError(err)
|
||||
require.Len(emails, 1)
|
||||
require.Empty(notFound)
|
||||
@@ -198,19 +207,22 @@ func TestSendingEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
update := EmailCreate{
|
||||
To: []EmailAddress{{Name: identity.Name, Email: identity.Email}},
|
||||
From: []EmailAddress{{Name: fromName, Email: from.email}},
|
||||
To: []EmailAddress{{Name: to.description, Email: to.email}},
|
||||
Cc: []EmailAddress{{Name: cc.description, Email: cc.email}},
|
||||
Sender: []EmailAddress{sender},
|
||||
Keywords: toBoolMapS("test"),
|
||||
Subject: "testing 1234",
|
||||
Subject: subject,
|
||||
MailboxIds: toBoolMapS(mailboxPerRole[JmapMailboxRoleDrafts].Id),
|
||||
}
|
||||
updated, _, _, _, err := s.client.CreateEmail(accountId, update, created.Id, s.session, s.ctx, s.logger, "")
|
||||
updated, _, _, _, err := s.client.CreateEmail(accountId, update, created.Id, 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)
|
||||
emails, notFound, _, _, _, err := s.client.GetEmails(accountId, 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)
|
||||
@@ -229,8 +241,81 @@ func TestSendingEmails(t *testing.T) {
|
||||
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)
|
||||
sub, _, _, _, err := s.client.SubmitEmail(accountId, identity.Id, updated.Id, &move, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.NotEmpty(sub.Id)
|
||||
require.NotEmpty(sub.ThreadId)
|
||||
require.Equal(updated.Id, sub.EmailId)
|
||||
require.Equal(identity.Id, sub.IdentityId)
|
||||
require.Equal(sub.UndoStatus, UndoStatusPending) // this *might* be fragile: if the server is fast enough, would we get "final" here?
|
||||
require.Empty(sub.DsnBlobIds)
|
||||
require.Empty(sub.MdnBlobIds)
|
||||
require.Equal(from.email, sub.Envelope.MailFrom.Email)
|
||||
require.Nil(sub.Envelope.MailFrom.Parameters)
|
||||
require.Len(sub.Envelope.RcptTo, 2)
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: to.email})
|
||||
require.Contains(sub.Envelope.RcptTo, Address{Email: cc.email})
|
||||
require.NotZero(sub.SendAt)
|
||||
require.Len(sub.DeliveryStatus, 2)
|
||||
require.Contains(sub.DeliveryStatus, to.email)
|
||||
require.Contains(sub.DeliveryStatus, cc.email)
|
||||
|
||||
a := 0
|
||||
maxAttempts := 3
|
||||
delivery := sub.DeliveryStatus[to.email].Delivered
|
||||
|
||||
for delivery != DeliveredYes {
|
||||
require.NotEqual(DeliveredNo, delivery)
|
||||
a++
|
||||
if a >= maxAttempts {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
subs, notFound, _, _, _, err := s.client.GetEmailSubmissionStatus(accountId, []string{sub.Id}, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Empty(notFound)
|
||||
require.Contains(subs, sub.Id)
|
||||
delivery = subs[sub.Id].DeliveryStatus[to.email].Delivered
|
||||
}
|
||||
|
||||
require.Contains([]DeliveryStatusDelivered{DeliveredYes, DeliveredUnknown}, delivery)
|
||||
|
||||
for _, r := range []struct {
|
||||
user User
|
||||
accountId string
|
||||
session *Session
|
||||
}{{to, toAccountId, toSession}, {cc, ccAccountId, ccSession}} {
|
||||
mailboxes, _, _, _, err := s.client.GetAllMailboxes([]string{r.accountId}, r.session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
inboxId := ""
|
||||
for _, mailbox := range mailboxes[r.accountId] {
|
||||
if mailbox.Role == JmapMailboxRoleInbox {
|
||||
inboxId = mailbox.Id
|
||||
require.Equal(1, mailbox.TotalEmails)
|
||||
}
|
||||
}
|
||||
require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.name)
|
||||
|
||||
emails, _, _, _, err := s.client.QueryEmails([]string{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, r.session, s.ctx, s.logger, "", 0, 0, true, 0)
|
||||
require.NoError(err)
|
||||
require.Contains(emails, r.accountId)
|
||||
require.Len(emails[r.accountId].Emails, 1)
|
||||
received := emails[r.accountId].Emails[0]
|
||||
require.Len(received.From, 1)
|
||||
require.Equal(from.email, received.From[0].Email)
|
||||
require.Equal(fromName, received.From[0].Name)
|
||||
require.Len(received.Sender, 1)
|
||||
require.Equal(from.email, received.Sender[0].Email)
|
||||
require.Equal(from.description, received.Sender[0].Name)
|
||||
require.Len(received.To, 1)
|
||||
require.Equal(to.email, received.To[0].Email)
|
||||
require.Equal(to.description, received.To[0].Name)
|
||||
require.Len(received.Cc, 1)
|
||||
require.Equal(cc.email, received.Cc[0].Email)
|
||||
require.Equal(cc.description, received.Cc[0].Name)
|
||||
require.Equal(subject, received.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +354,34 @@ func matchEmail(t *testing.T, actual Email, expected filledMail, hasBodies bool)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StalwartTest) findInbox(t *testing.T, accountId string, session *Session) (string, string) {
|
||||
require := require.New(t)
|
||||
respByAccountId, sessionState, _, _, err := s.client.GetAllMailboxes([]string{accountId}, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.Len(respByAccountId, 1)
|
||||
require.Contains(respByAccountId, accountId)
|
||||
resp := respByAccountId[accountId]
|
||||
|
||||
mailboxesNameByRole := map[string]string{}
|
||||
mailboxesUnreadByRole := map[string]int{}
|
||||
for _, m := range resp {
|
||||
if m.Role != "" {
|
||||
mailboxesNameByRole[m.Role] = m.Name
|
||||
mailboxesUnreadByRole[m.Role] = m.UnreadEmails
|
||||
}
|
||||
}
|
||||
require.Contains(mailboxesNameByRole, "inbox")
|
||||
require.Contains(mailboxesUnreadByRole, "inbox")
|
||||
require.Zero(mailboxesUnreadByRole["inbox"])
|
||||
|
||||
inboxId := mailboxId("inbox", resp)
|
||||
require.NotEmpty(inboxId)
|
||||
inboxFolder := mailboxesNameByRole["inbox"]
|
||||
require.NotEmpty(inboxFolder)
|
||||
return inboxId, inboxFolder
|
||||
}
|
||||
|
||||
var emailSplitter = regexp.MustCompile("(.+)@(.+)$")
|
||||
|
||||
func htmlFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder {
|
||||
@@ -371,8 +484,8 @@ var allKeywords = map[string]imap.Flag{
|
||||
JmapKeywordSeen: imap.FlagSeen,
|
||||
}
|
||||
|
||||
func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool) ([]filledMail, int, error) {
|
||||
to := fmt.Sprintf("%s <%s>", s.userPersonName, s.userEmail)
|
||||
func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, user User) ([]filledMail, int, error) {
|
||||
to := fmt.Sprintf("%s <%s>", user.description, user.email)
|
||||
ccEvery := 2
|
||||
bccEvery := 3
|
||||
attachmentEvery := 2
|
||||
@@ -394,7 +507,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool)
|
||||
}
|
||||
}(c)
|
||||
|
||||
if err = c.Login(s.username, s.password).Wait(); err != nil {
|
||||
if err = c.Login(user.name, user.password).Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -442,7 +555,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool)
|
||||
domain := addressParts[0][2]
|
||||
|
||||
toName := displayName
|
||||
toAddress := fmt.Sprintf("%s@%s", s.username, domain)
|
||||
toAddress := fmt.Sprintf("%s@%s", user.name, domain)
|
||||
ccName1 := "Team Lead"
|
||||
ccAddress1 := fmt.Sprintf("lead@%s", domain)
|
||||
ccName2 := "Coworker"
|
||||
|
||||
@@ -38,7 +38,10 @@ func TestEvents(t *testing.T) {
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count)
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count, session, user)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(accountId)
|
||||
require.NotEmpty(calendarId)
|
||||
@@ -50,7 +53,7 @@ func TestEvents(t *testing.T) {
|
||||
{Property: CalendarEventPropertyCreated, IsAscending: true},
|
||||
}
|
||||
|
||||
contactsByAccount, _, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, s.session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
|
||||
contactsByAccount, _, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
|
||||
require.NoError(err)
|
||||
|
||||
require.Len(contactsByAccount, 1)
|
||||
@@ -85,9 +88,11 @@ type EventsBoxes struct {
|
||||
func (s *StalwartTest) fillEvents(
|
||||
t *testing.T,
|
||||
count uint,
|
||||
session *Session,
|
||||
user User,
|
||||
) (string, string, map[string]CalendarEvent, EventsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(s.session, s.username, s.password, true, true)
|
||||
c, err := NewTestJmapClient(session, user.name, user.password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
@@ -245,7 +250,7 @@ func (s *StalwartTest) fillEvents(
|
||||
virtualLocationId: virtualLocationObj,
|
||||
},
|
||||
Alerts: map[string]jscalendar.Alert{
|
||||
alertId: jscalendar.Alert{
|
||||
alertId: {
|
||||
Type: jscalendar.AlertType,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
Type: jscalendar.OffsetTriggerType,
|
||||
|
||||
@@ -27,8 +27,6 @@ import (
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/tidwall/pretty"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
@@ -50,18 +48,33 @@ const (
|
||||
Wireshark = ""
|
||||
)
|
||||
|
||||
type User struct {
|
||||
name string
|
||||
description string
|
||||
email string
|
||||
password string
|
||||
}
|
||||
|
||||
func userpassword() string {
|
||||
password, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return password
|
||||
}
|
||||
|
||||
var (
|
||||
domains = [...]string{"earth.gov", "mars.mil", "opa.org", "acme.com"}
|
||||
people = [...]string{
|
||||
"Camina Drummer",
|
||||
"Amos Burton",
|
||||
"James Holden",
|
||||
"Anderson Dawes",
|
||||
"Naomi Nagata",
|
||||
"Klaes Ashford",
|
||||
"Fred Johnson",
|
||||
"Chrisjen Avasarala",
|
||||
"Bobby Draper",
|
||||
domains = [...]string{"earth.gov", "mars.mil", "opa.org"}
|
||||
users = [...]User{
|
||||
{"cdrummer", "Camina Drummer", "camina.drummer@opa.org", userpassword()},
|
||||
{"aburton", "Amos Burton", "amos.burton@earth.gov", userpassword()},
|
||||
{"jholden", "James Holden", "james.holden@earth.gov", userpassword()},
|
||||
{"adawes", "Anderson Dawes", "anderson.dawes@opa.org", userpassword()},
|
||||
{"nnagata", "Naomi Nagata", "naomi.nagata@opa.org", userpassword()},
|
||||
{"kashford", "Klaes Ashford", "klaes.ashford@opa.org", userpassword()},
|
||||
{"fjohnson", "Fred Johnson", "fred.johnson@opa.org", userpassword()},
|
||||
{"cavasarala", "Chrisjen Avasarala}", "chrissy@earth.gov", userpassword()},
|
||||
{"bdraper", "Roberta Draper", "bobby@mars.mil", userpassword()},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -70,22 +83,16 @@ const (
|
||||
httpPort = "8080"
|
||||
imapsPort = "993"
|
||||
configTemplate = `
|
||||
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
|
||||
authentication.fallback-admin.secret = "secret"
|
||||
authentication.fallback-admin.user = "mailadmin"
|
||||
authentication.master.secret = "{{.masterpassword}}"
|
||||
authentication.master.user = "{{.masterusername}}"
|
||||
directory.memory.principals.0000.class = "admin"
|
||||
directory.memory.principals.0000.description = "Superuser"
|
||||
directory.memory.principals.0000.email.0000 = "admin@example.org"
|
||||
directory.memory.principals.0000.name = "admin"
|
||||
directory.memory.principals.0000.secret = "secret"
|
||||
directory.memory.principals.0001.class = "individual"
|
||||
directory.memory.principals.0001.description = "{{.description}}"
|
||||
directory.memory.principals.0001.email.0000 = "{{.email}}"
|
||||
directory.memory.principals.0001.name = "{{.username}}"
|
||||
directory.memory.principals.0001.secret = "{{.password}}"
|
||||
directory.memory.principals.0001.storage.directory = "memory"
|
||||
directory.memory.type = "memory"
|
||||
directory.test.bind.auth.method = "default"
|
||||
directory.test.cache.size = 1048576
|
||||
directory.test.cache.ttl.negative = "10m"
|
||||
directory.test.cache.ttl.positive = "1h"
|
||||
directory.test.store = "rocksdb"
|
||||
directory.test.type = "internal"
|
||||
metrics.prometheus.enable = false
|
||||
server.listener.http.bind = "[::]:{{.httpPort}}"
|
||||
server.listener.http.protocol = "http"
|
||||
@@ -100,7 +107,7 @@ server.socket.reuse-addr = true
|
||||
server.socket.reuse-port = true
|
||||
storage.blob = "rocksdb"
|
||||
storage.data = "rocksdb"
|
||||
storage.directory = "memory"
|
||||
storage.directory = "test"
|
||||
storage.fts = "rocksdb"
|
||||
storage.lookup = "rocksdb"
|
||||
store.rocksdb.compression = "lz4"
|
||||
@@ -114,6 +121,14 @@ tracer.log.lossy = false
|
||||
tracer.log.multiline = false
|
||||
tracer.log.type = "stdout"
|
||||
sharing.allow-directory-query = false
|
||||
auth.dkim.sign = false
|
||||
auth.dkim.verify = "disable"
|
||||
auth.spf.verify.ehlo = "disable"
|
||||
auth.spf.verify.mail-from = "disable"
|
||||
auth.arc.verify = "disable"
|
||||
auth.arc.seal = false
|
||||
auth.dmarc.verify = "disable"
|
||||
auth.iprev.verify = "disable"
|
||||
`
|
||||
)
|
||||
|
||||
@@ -134,19 +149,16 @@ func skip(t *testing.T) bool {
|
||||
}
|
||||
|
||||
type StalwartTest struct {
|
||||
t *testing.T
|
||||
ip string
|
||||
imapPort int
|
||||
container *testcontainers.DockerContainer
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
client *Client
|
||||
session *Session
|
||||
username string
|
||||
password string
|
||||
logger *clog.Logger
|
||||
userPersonName string
|
||||
userEmail string
|
||||
t *testing.T
|
||||
ip string
|
||||
imapPort int
|
||||
container *testcontainers.DockerContainer
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
client *Client
|
||||
logger *clog.Logger
|
||||
jmapBaseUrl *url.URL
|
||||
sessionUrl *url.URL
|
||||
|
||||
io.Closer
|
||||
}
|
||||
@@ -162,6 +174,38 @@ func (s *StalwartTest) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StalwartTest) Session(username string) *Session {
|
||||
session, jerr := s.client.FetchSession(s.sessionUrl, username, s.logger)
|
||||
require.NoError(s.t, jerr)
|
||||
require.NotNil(s.t, session.Capabilities.Mail)
|
||||
require.NotNil(s.t, session.Capabilities.Calendars)
|
||||
require.NotNil(s.t, session.Capabilities.Contacts)
|
||||
|
||||
// we have to overwrite the hostname in JMAP URL because the container
|
||||
// will know its name to be a random Docker container identifier, or
|
||||
// "localhost" as we defined the hostname in the Stalwart configuration,
|
||||
// and we also need to overwrite the port number as its not mapped
|
||||
session.JmapUrl.Host = s.jmapBaseUrl.Host
|
||||
session.WebsocketUrl.Host = s.jmapBaseUrl.Host
|
||||
var err error
|
||||
session.ApiUrl, err = replaceHost(session.ApiUrl, s.jmapBaseUrl.Host)
|
||||
require.NoError(s.t, err)
|
||||
session.DownloadUrl, err = replaceHost(session.DownloadUrl, s.jmapBaseUrl.Host)
|
||||
require.NoError(s.t, err)
|
||||
session.UploadUrl, err = replaceHost(session.UploadUrl, s.jmapBaseUrl.Host)
|
||||
require.NoError(s.t, err)
|
||||
session.EventSourceUrl, err = replaceHost(session.EventSourceUrl, s.jmapBaseUrl.Host)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
return &session
|
||||
}
|
||||
|
||||
type stalwartTestLogConsumer struct{}
|
||||
|
||||
func (lc *stalwartTestLogConsumer) Accept(l testcontainers.Log) {
|
||||
fmt.Print("STALWART: " + string(l.Content))
|
||||
}
|
||||
|
||||
func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
var _ context.CancelFunc = cancel // ignore context leak warning: it is passed in the struct and called in Close()
|
||||
@@ -189,33 +233,11 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
masterPasswordHash = digest.Encode()
|
||||
}
|
||||
|
||||
usernameSuffix, err := pw.Generate(8, 2, 0, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
username := "user_" + usernameSuffix
|
||||
|
||||
password, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostname := "localhost"
|
||||
|
||||
userPersonName := people[rand.Intn(len(people))]
|
||||
var userEmail string
|
||||
{
|
||||
domain := domains[rand.Intn(len(domains))]
|
||||
userEmail = strings.Join(strings.Split(cases.Lower(language.English).String(userPersonName), " "), ".") + "@" + domain
|
||||
}
|
||||
|
||||
configBuf := bytes.NewBufferString("")
|
||||
template.Must(template.New("").Parse(configTemplate)).Execute(configBuf, map[string]any{
|
||||
"hostname": hostname,
|
||||
"password": password,
|
||||
"username": username,
|
||||
"description": userPersonName,
|
||||
"email": userEmail,
|
||||
"masterusername": masterUsername,
|
||||
"masterpassword": masterPasswordHash,
|
||||
"httpPort": httpPort,
|
||||
@@ -227,6 +249,7 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
container, err := testcontainers.Run(
|
||||
ctx,
|
||||
stalwartImage,
|
||||
testcontainers.WithLogConsumers(&stalwartTestLogConsumer{}),
|
||||
testcontainers.WithExposedPorts(httpPort+"/tcp", imapsPort+"/tcp"),
|
||||
testcontainers.WithFiles(testcontainers.ContainerFile{
|
||||
Reader: configReader,
|
||||
@@ -262,7 +285,8 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
loggerImpl := clog.NewLogger(clog.Level("trace"))
|
||||
logger := &loggerImpl
|
||||
var j Client
|
||||
var session *Session
|
||||
var jmapBaseUrl *url.URL
|
||||
var sessionUrl *url.URL
|
||||
{
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = time.Duration(30 * time.Second)
|
||||
@@ -279,11 +303,12 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jmapBaseUrl := url.URL{
|
||||
jmapBaseUrl = &url.URL{
|
||||
Scheme: "http",
|
||||
Host: ip + ":" + jmapPort.Port(),
|
||||
Path: "/",
|
||||
}
|
||||
sessionUrl = jmapBaseUrl.JoinPath(".well-known", "jmap")
|
||||
|
||||
if Wireshark != "" {
|
||||
fmt.Printf("\x1b[45;37;1m Starting Wireshark on port %v \x1b[0m\n", jmapPort)
|
||||
@@ -301,8 +326,6 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
sessionUrl := jmapBaseUrl.JoinPath(".well-known", "jmap")
|
||||
|
||||
eventListener := nullHttpJmapApiClientEventListener{}
|
||||
|
||||
api := NewHttpJmapClient(
|
||||
@@ -318,47 +341,139 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) {
|
||||
}
|
||||
|
||||
j = NewClient(api, api, api, wscf)
|
||||
s, err := j.FetchSession(sessionUrl, username, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// we have to overwrite the hostname in JMAP URL because the container
|
||||
// will know its name to be a random Docker container identifier, or
|
||||
// "localhost" as we defined the hostname in the Stalwart configuration,
|
||||
// and we also need to overwrite the port number as its not mapped
|
||||
s.JmapUrl.Host = jmapBaseUrl.Host
|
||||
s.WebsocketUrl.Host = jmapBaseUrl.Host
|
||||
//s.JmapEndpoint = jmapBaseUrl.Host
|
||||
s.ApiUrl, err = replaceHost(s.ApiUrl, jmapBaseUrl.Host)
|
||||
require.NoError(t, err)
|
||||
s.DownloadUrl, err = replaceHost(s.DownloadUrl, jmapBaseUrl.Host)
|
||||
require.NoError(t, err)
|
||||
s.UploadUrl, err = replaceHost(s.UploadUrl, jmapBaseUrl.Host)
|
||||
require.NoError(t, err)
|
||||
s.EventSourceUrl, err = replaceHost(s.EventSourceUrl, jmapBaseUrl.Host)
|
||||
require.NoError(t, err)
|
||||
session = &s
|
||||
}
|
||||
|
||||
require.NotNil(t, session.Capabilities.Mail)
|
||||
require.NotNil(t, session.Capabilities.Calendars)
|
||||
require.NotNil(t, session.Capabilities.Contacts)
|
||||
// provision some things using Stalwart's Management API
|
||||
{
|
||||
var h http.Client
|
||||
{
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = time.Duration(30 * time.Second)
|
||||
tr.TLSClientConfig = tlsConfig
|
||||
h = *http.DefaultClient
|
||||
h.Transport = tr
|
||||
}
|
||||
|
||||
apiPort, err := container.MappedPort(ctx, httpPort)
|
||||
require.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("http://%s:%d/api/principal", ip, apiPort.Int())
|
||||
|
||||
for _, domain := range domains {
|
||||
fmt.Printf("Creating domain '%v'\n", domain)
|
||||
bb, err := json.Marshal(map[string]any{
|
||||
"type": "domain",
|
||||
"name": domain,
|
||||
"description": domain,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bb))
|
||||
require.NoError(t, err)
|
||||
req.SetBasicAuth("mailadmin", "secret")
|
||||
resp, err := h.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "200 OK", resp.Status)
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Printf("Creating individual '%v'\n", user.name)
|
||||
bb, err := json.Marshal(map[string]any{
|
||||
"type": "individual",
|
||||
"name": user.name,
|
||||
"description": user.description,
|
||||
"emails": user.email,
|
||||
"roles": []string{"user"},
|
||||
"secrets": user.password,
|
||||
"quota": 20000000000,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bb))
|
||||
require.NoError(t, err)
|
||||
req.SetBasicAuth("mailadmin", "secret")
|
||||
resp, err := h.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "200 OK", resp.Status)
|
||||
|
||||
// fetch the user once with the superadmin credentials to "activate" it,
|
||||
// it is unclear why that is needed, but without that, we get errors back
|
||||
// that we are not allowed to access that resource
|
||||
{
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
req.SetBasicAuth("mailadmin", "secret")
|
||||
resp, err := h.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "200 OK", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
req.SetBasicAuth("mailadmin", "secret")
|
||||
resp, err := h.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "200 OK", resp.Status)
|
||||
var list struct {
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
Items []struct {
|
||||
Type string `json:"type"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Emails []string `json:"emails"`
|
||||
Roles []string `json:"roles"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
bb, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
err = json.Unmarshal(bb, &list)
|
||||
require.NoError(t, err)
|
||||
individuals := []struct {
|
||||
Id int
|
||||
Name string
|
||||
Emails []string
|
||||
Roles []string
|
||||
}{}
|
||||
for _, p := range list.Data.Items {
|
||||
if p.Type == "individual" {
|
||||
individuals = append(individuals, struct {
|
||||
Id int
|
||||
Name string
|
||||
Emails []string
|
||||
Roles []string
|
||||
}{p.Id, p.Name, p.Emails, p.Roles})
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, len(users), len(individuals))
|
||||
}
|
||||
|
||||
{
|
||||
// check whether we can fetch a session for the provisioned users
|
||||
for _, user := range users {
|
||||
session, err := j.FetchSession(sessionUrl, user.name, logger)
|
||||
require.NoError(t, err, "failed to retrieve JMAP session for newly created principal '%s'", user.name)
|
||||
require.Equal(t, user.name, session.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success = true
|
||||
return &StalwartTest{
|
||||
t: t,
|
||||
ip: ip,
|
||||
imapPort: imapPort.Int(),
|
||||
container: container,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
client: &j,
|
||||
session: session,
|
||||
username: username,
|
||||
password: password,
|
||||
logger: logger,
|
||||
userPersonName: userPersonName,
|
||||
userEmail: userEmail,
|
||||
t: t,
|
||||
ip: ip,
|
||||
imapPort: imapPort.Int(),
|
||||
container: container,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
client: &j,
|
||||
logger: logger,
|
||||
jmapBaseUrl: jmapBaseUrl,
|
||||
sessionUrl: sessionUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -372,20 +487,6 @@ func replaceHost(u string, host string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func pickOneRandomlyFromMap[K comparable, V any](m map[K]V) (K, V) {
|
||||
l := rand.Intn(len(m))
|
||||
i := 0
|
||||
for k, v := range m {
|
||||
if i == l {
|
||||
return k, v
|
||||
}
|
||||
i++
|
||||
}
|
||||
panic("map is empty")
|
||||
}
|
||||
*/
|
||||
|
||||
func pickRandomlyFromMap[K comparable, V any](m map[K]V, min int, max int) map[K]V {
|
||||
if min < 0 || max < 0 {
|
||||
panic("min and max must be >= 0")
|
||||
@@ -1023,6 +1124,10 @@ func pickRandom[T any](s ...T) T {
|
||||
return s[rand.Intn(len(s))]
|
||||
}
|
||||
|
||||
func pickUser() User {
|
||||
return users[rand.Intn(len(users))]
|
||||
}
|
||||
|
||||
func pickRandoms[T any](s ...T) []T {
|
||||
n := rand.Intn(len(s))
|
||||
if n == 0 {
|
||||
|
||||
@@ -64,13 +64,16 @@ func TestWs(t *testing.T) {
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
mailAccountId := s.session.PrimaryAccounts.Mail
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
mailAccountId := session.PrimaryAccounts.Mail
|
||||
inboxFolder := ""
|
||||
{
|
||||
_, inboxFolder = s.findInbox(t, mailAccountId)
|
||||
_, inboxFolder = s.findInbox(t, mailAccountId, session)
|
||||
}
|
||||
|
||||
l := &testWsPushListener{t: t, username: s.username, logger: s.logger, mailAccountId: mailAccountId}
|
||||
l := &testWsPushListener{t: t, username: user.name, logger: s.logger, mailAccountId: mailAccountId}
|
||||
s.client.AddWsPushListener(l)
|
||||
|
||||
require.Equal(uint32(0), l.calls.Load())
|
||||
@@ -84,9 +87,9 @@ func TestWs(t *testing.T) {
|
||||
|
||||
var initialState State
|
||||
{
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, s.session, s.ctx, s.logger, "", "", 0)
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", "", 0)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.NotEmpty(state)
|
||||
//fmt.Printf("\x1b[45;1;4mChanges [%s]:\x1b[0m\n", state)
|
||||
//for _, c := range changes.Created { fmt.Printf("%s %s\n", c.Id, c.Subject) }
|
||||
@@ -98,9 +101,9 @@ func TestWs(t *testing.T) {
|
||||
require.NotEmpty(initialState)
|
||||
|
||||
{
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, s.session, s.ctx, s.logger, "", initialState, 0)
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, 0)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.Equal(initialState, state)
|
||||
require.Equal(initialState, changes.NewState)
|
||||
require.Empty(changes.Created)
|
||||
@@ -108,7 +111,7 @@ func TestWs(t *testing.T) {
|
||||
require.Empty(changes.Updated)
|
||||
}
|
||||
|
||||
wsc, err := s.client.EnablePushNotifications(initialState, func() (*Session, error) { return s.session, nil })
|
||||
wsc, err := s.client.EnablePushNotifications(initialState, func() (*Session, error) { return session, nil })
|
||||
require.NoError(err)
|
||||
defer wsc.Close()
|
||||
|
||||
@@ -124,7 +127,7 @@ func TestWs(t *testing.T) {
|
||||
emailIds := []string{}
|
||||
|
||||
{
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false)
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false, user)
|
||||
require.NoError(err)
|
||||
require.Equal(1, n)
|
||||
}
|
||||
@@ -141,9 +144,9 @@ func TestWs(t *testing.T) {
|
||||
}
|
||||
var lastState State
|
||||
{
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, s.session, s.ctx, s.logger, "", initialState, 0)
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", initialState, 0)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.NotEqual(initialState, state)
|
||||
require.NotEqual(initialState, changes.NewState)
|
||||
require.Equal(state, changes.NewState)
|
||||
@@ -156,7 +159,7 @@ func TestWs(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false)
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 1, false, user)
|
||||
require.NoError(err)
|
||||
require.Equal(1, n)
|
||||
}
|
||||
@@ -175,9 +178,9 @@ func TestWs(t *testing.T) {
|
||||
l.m.Unlock()
|
||||
}
|
||||
{
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, s.session, s.ctx, s.logger, "", lastState, 0)
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, 0)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.NotEqual(lastState, state)
|
||||
require.NotEqual(lastState, changes.NewState)
|
||||
require.Equal(state, changes.NewState)
|
||||
@@ -190,7 +193,7 @@ func TestWs(t *testing.T) {
|
||||
}
|
||||
|
||||
{
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 0, true)
|
||||
_, n, err := s.fillEmailsWithImap(inboxFolder, 0, true, user)
|
||||
require.NoError(err)
|
||||
require.Equal(0, n)
|
||||
}
|
||||
@@ -209,9 +212,9 @@ func TestWs(t *testing.T) {
|
||||
l.m.Unlock()
|
||||
}
|
||||
{
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, s.session, s.ctx, s.logger, "", lastState, 0)
|
||||
changes, sessionState, state, _, err := s.client.GetEmailChanges(mailAccountId, session, s.ctx, s.logger, "", lastState, 0)
|
||||
require.NoError(err)
|
||||
require.Equal(s.session.State, sessionState)
|
||||
require.Equal(session.State, sessionState)
|
||||
require.NotEqual(lastState, state)
|
||||
require.NotEqual(lastState, changes.NewState)
|
||||
require.Equal(state, changes.NewState)
|
||||
|
||||
@@ -2358,6 +2358,11 @@ type Email struct {
|
||||
Preview string `json:"preview,omitempty"`
|
||||
}
|
||||
|
||||
type AddressParameters struct {
|
||||
HoldUntil time.Time `json:"HOLDUNTIL,omitzero"`
|
||||
HoldForSeconds uint `json:"HOLDFOR,omitzero"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
// The email address being represented by the object.
|
||||
//
|
||||
@@ -2373,7 +2378,7 @@ type Address struct {
|
||||
// or null if the parameter does not take a value.
|
||||
//
|
||||
// [RFC5321]: https://datatracker.ietf.org/doc/html/rfc5321
|
||||
Parameters map[string]any `json:"parameters,omitempty"` // TODO RFC5321
|
||||
Parameters *AddressParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// Information for use when sending via SMTP.
|
||||
@@ -2987,6 +2992,20 @@ type EmailCreate struct {
|
||||
// The value for each key in the object MUST be true.
|
||||
Keywords map[string]bool `json:"keywords,omitempty"`
|
||||
|
||||
// This is a list of all header fields [RFC5322], in the same order they appear in the message.
|
||||
//
|
||||
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
|
||||
Headers []EmailHeader `json:"headers,omitempty"`
|
||||
|
||||
// The value is identical to the value of header:In-Reply-To:asMessageIds.
|
||||
InReplyTo []string `json:"inReplyTo,omitempty"`
|
||||
|
||||
// The value is identical to the value of header:References:asMessageIds.
|
||||
References []string `json:"references,omitempty"`
|
||||
|
||||
// The value is identical to the value of header:Sender:asAddresses.
|
||||
Sender []EmailAddress `json:"sender,omitempty"`
|
||||
|
||||
// The ["From:" field] specifies the author(s) of the message, that is, the mailbox(es)
|
||||
// of the person(s) or system(s) responsible for the writing of the message
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user