474 lines
12 KiB
Go
474 lines
12 KiB
Go
|
package tests
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/bradenaw/juniper/iterator"
|
||
|
"github.com/bradenaw/juniper/xslices"
|
||
|
"github.com/cucumber/godog"
|
||
|
"github.com/cucumber/messages-go/v16"
|
||
|
"github.com/emersion/go-imap"
|
||
|
id "github.com/emersion/go-imap-id"
|
||
|
"github.com/emersion/go-imap/client"
|
||
|
"github.com/google/go-cmp/cmp"
|
||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||
|
"golang.org/x/exp/slices"
|
||
|
)
|
||
|
|
||
|
func (s *scenario) userConnectsIMAPClient(username, clientID string) error {
|
||
|
return s.t.newIMAPClient(s.t.getUserID(username), clientID)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) userConnectsIMAPClientOnPort(username, clientID string, port int) error {
|
||
|
return s.t.newIMAPClientOnPort(s.t.getUserID(username), clientID, port)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) userConnectsAndAuthenticatesIMAPClient(username, clientID string) error {
|
||
|
if err := s.t.newIMAPClient(s.t.getUserID(username), clientID); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
userID, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
return client.Login(s.t.getUserAddr(userID), s.t.getUserPass(userID))
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCanAuthenticate(clientID string) error {
|
||
|
userID, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
return client.Login(s.t.getUserAddr(userID), s.t.getUserPass(userID))
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCannotAuthenticate(clientID string) error {
|
||
|
userID, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
if err := client.Login(s.t.getUserAddr(userID), s.t.getUserPass(userID)); err == nil {
|
||
|
return fmt.Errorf("expected error, got nil")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID string) error {
|
||
|
userID, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
if err := client.Login(s.t.getUserAddr(userID)+"bad", s.t.getUserPass(userID)); err == nil {
|
||
|
return fmt.Errorf("expected error, got nil")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID string) error {
|
||
|
userID, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
if err := client.Login(s.t.getUserAddr(userID), s.t.getUserPass(userID)+"bad"); err == nil {
|
||
|
return fmt.Errorf("expected error, got nil")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientAnnouncesItsIDWithNameAndVersion(clientID, name, version string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
if _, err := id.NewClient(client).ID(id.ID{id.FieldName: name, id.FieldVersion: version}); err != nil {
|
||
|
return fmt.Errorf("expected error, got nil")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCreatesMailbox(clientID, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
s.t.pushError(client.Create(mailbox))
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientDeletesMailbox(clientID, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
s.t.pushError(client.Delete(mailbox))
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientRenamesMailboxTo(clientID, fromName, toName string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
s.t.pushError(client.Rename(fromName, toName))
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesTheFollowingMailboxInfo(clientID string, table *godog.Table) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
status, err := clientStatus(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
haveMailboxes := xslices.Map(status, func(info *imap.MailboxStatus) Mailbox {
|
||
|
return Mailbox{
|
||
|
Name: info.Name,
|
||
|
Total: int(info.Messages),
|
||
|
Unread: int(info.Unseen),
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return matchMailboxes(haveMailboxes, table)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesTheFollowingMailboxInfoForMailbox(clientID, mailbox string, table *godog.Table) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
status, err := clientStatus(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
status = xslices.Filter(status, func(status *imap.MailboxStatus) bool {
|
||
|
return status.Name == mailbox
|
||
|
})
|
||
|
|
||
|
haveMailboxes := xslices.Map(status, func(info *imap.MailboxStatus) Mailbox {
|
||
|
return Mailbox{
|
||
|
Name: info.Name,
|
||
|
Total: int(info.Messages),
|
||
|
Unread: int(info.Unseen),
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return matchMailboxes(haveMailboxes, table)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesTheFollowingMailboxes(clientID string, table *godog.Table) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
mailboxes, err := clientList(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
have := xslices.Map(mailboxes, func(info *imap.MailboxInfo) string {
|
||
|
return info.Name
|
||
|
})
|
||
|
|
||
|
want := xslices.Map(table.Rows[1:], func(row *messages.PickleTableRow) string {
|
||
|
return row.Cells[0].Value
|
||
|
})
|
||
|
|
||
|
if !cmp.Equal(want, have, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
|
||
|
return fmt.Errorf("want %v, have %v", want, have)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesMailbox(clientID, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
mailboxes, err := clientList(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if !slices.Contains(xslices.Map(mailboxes, func(info *imap.MailboxInfo) string { return info.Name }), mailbox) {
|
||
|
return fmt.Errorf("expected %v to contain %v but it doesn't", mailboxes, mailbox)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientDoesNotSeeMailbox(clientID, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
mailboxes, err := clientList(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if slices.Contains(xslices.Map(mailboxes, func(info *imap.MailboxInfo) string { return info.Name }), mailbox) {
|
||
|
return fmt.Errorf("expected %v to not contain %v but it does", mailboxes, mailbox)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCountsMailboxesUnder(clientID string, count int, parent string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
mailboxes, err := clientList(client)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
mailboxes = xslices.Filter(mailboxes, func(info *imap.MailboxInfo) bool {
|
||
|
return strings.HasPrefix(info.Name, parent) && info.Name != parent
|
||
|
})
|
||
|
|
||
|
if len(mailboxes) != count {
|
||
|
return fmt.Errorf("expected %v to have %v mailboxes, got %v", parent, count, len(mailboxes))
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSelectsMailbox(clientID, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
status, err := client.Select(mailbox, false)
|
||
|
if err != nil {
|
||
|
s.t.pushError(err)
|
||
|
} else if status.Name != mailbox {
|
||
|
return fmt.Errorf("expected mailbox %v, got %v", mailbox, status.Name)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCopiesTheMessageWithSubjectFromTo(clientID, subject, from, to string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
uid, err := clientGetUIDBySubject(client, from, subject)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return clientCopy(client, from, to, uid)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientCopiesAllMessagesFromTo(clientID, from, to string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
return clientCopy(client, from, to)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
fetch, err := clientFetch(client, mailbox)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
haveMessages := xslices.Map(fetch, func(msg *imap.Message) Message {
|
||
|
return Message{
|
||
|
Sender: msg.Envelope.Sender[0].Address(),
|
||
|
Recipient: msg.Envelope.To[0].Address(),
|
||
|
Subject: msg.Envelope.Subject,
|
||
|
Unread: slices.Contains(msg.Flags, imap.SeenFlag),
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return matchMessages(haveMessages, table)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientEventuallySeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error {
|
||
|
return eventually(
|
||
|
func() error { return s.imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox, table) },
|
||
|
5*time.Second,
|
||
|
500*time.Millisecond,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesMessagesInMailbox(clientID string, count int, mailbox string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
fetch, err := clientFetch(client, mailbox)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if len(fetch) != count {
|
||
|
return fmt.Errorf("expected mailbox %v to be empty, got %v", mailbox, fetch)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientEventuallySeesMessagesInMailbox(clientID string, count int, mailbox string) error {
|
||
|
return eventually(
|
||
|
func() error { return s.imapClientSeesMessagesInMailbox(clientID, count, mailbox) },
|
||
|
5*time.Second,
|
||
|
500*time.Millisecond,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientMarksMessageAsDeleted(clientID string, seq int) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
_, err := clientStore(client, seq, seq, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientMarksMessageAsNotDeleted(clientID string, seq int) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
_, err := clientStore(client, seq, seq, imap.FormatFlagsOp(imap.RemoveFlags, true), imap.DeletedFlag)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientMarksAllMessagesAsDeleted(clientID string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
_, err := clientStore(client, 1, int(client.Mailbox().Messages), imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientSeesThatMessageHasTheFlag(clientID string, seq int, flag string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
fetch, err := clientFetch(client, client.Mailbox().Name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
idx := xslices.IndexFunc(fetch, func(msg *imap.Message) bool {
|
||
|
return msg.SeqNum == uint32(seq)
|
||
|
})
|
||
|
|
||
|
if !slices.Contains(fetch[idx].Flags, flag) {
|
||
|
return fmt.Errorf("expected message %v to have flag %v, got %v", seq, flag, fetch[idx].Flags)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *scenario) imapClientExpunges(clientID string) error {
|
||
|
_, client := s.t.getIMAPClient(clientID)
|
||
|
|
||
|
return client.Expunge(nil)
|
||
|
}
|
||
|
|
||
|
func clientList(client *client.Client) ([]*imap.MailboxInfo, error) {
|
||
|
resCh := make(chan *imap.MailboxInfo)
|
||
|
|
||
|
go func() {
|
||
|
if err := client.List("", "*", resCh); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return iterator.Collect(iterator.Chan(resCh)), nil
|
||
|
}
|
||
|
|
||
|
func clientStatus(client *client.Client) ([]*imap.MailboxStatus, error) {
|
||
|
var status []*imap.MailboxStatus
|
||
|
|
||
|
list, err := clientList(client)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for _, info := range list {
|
||
|
res, err := client.Status(info.Name, []imap.StatusItem{imap.StatusMessages, imap.StatusRecent, imap.StatusUidNext, imap.StatusUidValidity, imap.StatusUnseen})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
status = append(status, res)
|
||
|
}
|
||
|
|
||
|
return status, nil
|
||
|
}
|
||
|
|
||
|
func clientGetUIDBySubject(client *client.Client, mailbox, subject string) (uint32, error) {
|
||
|
fetch, err := clientFetch(client, mailbox)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
|
||
|
for _, msg := range fetch {
|
||
|
if msg.Envelope.Subject == subject {
|
||
|
return msg.Uid, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0, fmt.Errorf("could not find message with subject %v", subject)
|
||
|
}
|
||
|
|
||
|
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) {
|
||
|
status, err := client.Select(mailbox, false)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if status.Messages == 0 {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
resCh := make(chan *imap.Message)
|
||
|
|
||
|
go func() {
|
||
|
if err := client.Fetch(
|
||
|
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
||
|
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid},
|
||
|
resCh,
|
||
|
); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return iterator.Collect(iterator.Chan(resCh)), nil
|
||
|
}
|
||
|
|
||
|
func clientCopy(client *client.Client, from, to string, uid ...uint32) error {
|
||
|
status, err := client.Select(from, false)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if status.Messages == 0 {
|
||
|
return fmt.Errorf("expected %v to have messages, but it doesn't", from)
|
||
|
}
|
||
|
|
||
|
var seqset *imap.SeqSet
|
||
|
|
||
|
if len(uid) == 0 {
|
||
|
seqset = &imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}}
|
||
|
} else {
|
||
|
seqset = &imap.SeqSet{}
|
||
|
|
||
|
for _, uid := range uid {
|
||
|
seqset.AddNum(uid)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return client.UidCopy(seqset, to)
|
||
|
}
|
||
|
|
||
|
func clientStore(client *client.Client, from, to int, item imap.StoreItem, flags ...string) ([]*imap.Message, error) {
|
||
|
resCh := make(chan *imap.Message)
|
||
|
|
||
|
go func() {
|
||
|
if err := client.Store(
|
||
|
&imap.SeqSet{Set: []imap.Seq{{Start: uint32(from), Stop: uint32(to)}}},
|
||
|
item,
|
||
|
xslices.Map(flags, func(flag string) interface{} { return flag }),
|
||
|
resCh,
|
||
|
); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return iterator.Collect(iterator.Chan(resCh)), nil
|
||
|
}
|