mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
When an IMAP client appends a message to a mailbox, it can specify which flags it wants the appended message to have. We need to handle these in a proton-specific way; not-seen messages need to be imported with the Unread bool set to true, and flagged messages need to additionally be imported with the Starred label.
517 lines
14 KiB
Go
517 lines
14 KiB
Go
// Copyright (c) 2022 Proton AG
|
|
//
|
|
// This file is part of Proton Mail Bridge.
|
|
//
|
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package tests
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"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 {
|
|
return s.userConnectsAndAuthenticatesIMAPClientWithAddress(username, clientID, s.t.getUserAddrs(s.t.getUserID(username))[0])
|
|
}
|
|
|
|
func (s *scenario) userConnectsAndAuthenticatesIMAPClientWithAddress(username, clientID, address 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(address, s.t.getUserBridgePass(userID))
|
|
}
|
|
|
|
func (s *scenario) imapClientCanAuthenticate(clientID string) error {
|
|
userID, client := s.t.getIMAPClient(clientID)
|
|
|
|
return client.Login(s.t.getUserAddrs(userID)[0], s.t.getUserBridgePass(userID))
|
|
}
|
|
|
|
func (s *scenario) imapClientCanAuthenticateWithAddress(clientID string, address string) error {
|
|
userID, client := s.t.getIMAPClient(clientID)
|
|
|
|
return client.Login(address, s.t.getUserBridgePass(userID))
|
|
}
|
|
|
|
func (s *scenario) imapClientCannotAuthenticate(clientID string) error {
|
|
userID, client := s.t.getIMAPClient(clientID)
|
|
|
|
if err := client.Login(s.t.getUserAddrs(userID)[0], s.t.getUserBridgePass(userID)); err == nil {
|
|
return fmt.Errorf("expected error, got nil")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address string) error {
|
|
userID, client := s.t.getIMAPClient(clientID)
|
|
|
|
if err := client.Login(address, s.t.getUserBridgePass(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.getUserAddrs(userID)[0]+"bad", s.t.getUserBridgePass(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.getUserAddrs(userID)[0], s.t.getUserBridgePass(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, newMailboxFromIMAP)
|
|
|
|
wantMailboxes, err := unmarshalTable[Mailbox](table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return matchMailboxes(haveMailboxes, wantMailboxes)
|
|
}
|
|
|
|
func (s *scenario) imapClientEventuallySeesTheFollowingMailboxInfo(clientID string, table *godog.Table) error {
|
|
return eventually(func() error {
|
|
return s.imapClientSeesTheFollowingMailboxInfo(clientID, 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, newMailboxFromIMAP)
|
|
|
|
wantMailboxes, err := unmarshalTable[Mailbox](table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return matchMailboxes(haveMailboxes, wantMailboxes)
|
|
}
|
|
|
|
func (s *scenario) imapClientSeesTheFollowingMailboxes(clientID string, table *godog.Table) error {
|
|
_, client := s.t.getIMAPClient(clientID)
|
|
|
|
mailboxes := clientList(client)
|
|
|
|
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 := clientList(client)
|
|
|
|
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 := clientList(client)
|
|
|
|
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 := clientList(client)
|
|
|
|
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, newMessageFromIMAP)
|
|
|
|
wantMessages, err := unmarshalTable[Message](table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return matchMessages(haveMessages, wantMessages)
|
|
}
|
|
|
|
func (s *scenario) imapClientEventuallySeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error {
|
|
return eventually(func() error {
|
|
return s.imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox, table)
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
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 (s *scenario) imapClientAppendsTheFollowingMessageToMailbox(clientID string, mailbox string, docString *godog.DocString) error {
|
|
_, client := s.t.getIMAPClient(clientID)
|
|
|
|
return clientAppend(client, mailbox, docString.Content)
|
|
}
|
|
|
|
func (s *scenario) imapClientAppendsToMailbox(clientID string, file, mailbox string) error {
|
|
_, client := s.t.getIMAPClient(clientID)
|
|
|
|
b, err := os.ReadFile(filepath.Join("testdata", file))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return clientAppend(client, mailbox, string(b))
|
|
}
|
|
|
|
func clientList(client *client.Client) []*imap.MailboxInfo {
|
|
resCh := make(chan *imap.MailboxInfo)
|
|
|
|
go func() {
|
|
if err := client.List("", "*", resCh); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
return iterator.Collect(iterator.Chan(resCh))
|
|
}
|
|
|
|
func clientStatus(client *client.Client) ([]*imap.MailboxStatus, error) {
|
|
list := clientList(client)
|
|
|
|
status := make([]*imap.MailboxStatus, 0, len(list))
|
|
|
|
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, "BODY.PEEK[]"},
|
|
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) { //nolint:unparam
|
|
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
|
|
}
|
|
|
|
func clientAppend(client *client.Client, mailbox string, literal string) error {
|
|
return client.Append(mailbox, []string{}, time.Now(), strings.NewReader(literal))
|
|
}
|