Compare commits

...

14 Commits

Author SHA1 Message Date
d4198737a6 Other: Bridge Perth Narrows v3.0.5 2022-12-05 15:42:49 +01:00
04881b9b78 GODT-2178: Bump go-proton-api to fix drafts 2022-12-05 15:14:30 +01:00
990b8cda96 GODT-2180: Allow login with FIDO2
The API docs didn't specify what the "integer" meant. Turns out it's a
bitfield; we can't compare with equality.
2022-12-05 14:22:38 +01:00
27889b8085 Other: Bridge Perth Narrows v3.0.4 2022-12-02 15:42:11 +01:00
2cd7735468 Other: Do not list \Deleted flag for All Mail 2022-12-02 14:59:52 +01:00
8990f2d1d6 Other: Ensure expunge feature test pushes to error stack 2022-12-02 14:59:52 +01:00
7bc608ce6c GODT-2170: Use client-side draft update in integration tests 2022-12-02 13:27:19 +00:00
01c7daaba7 Other: Update gluon to latest version 2022-12-02 13:27:19 +00:00
8408a5fdc0 GODT-2170: Improving test server behaviour. 2022-12-02 13:27:19 +00:00
828fe0e86e GODT-2170: Update draft event means delete old and create new message. 2022-12-02 13:27:19 +00:00
5c3179df48 GODT-2170: User create draft rounte: first steps. 2022-12-02 13:27:19 +00:00
618cb27ac1 Other: Disable perma-delete for expunge on Spam folder 2022-12-02 13:43:53 +01:00
83a569b366 Other: Bridge Perth Narrows v3.0.3 2022-12-01 08:42:24 +01:00
70244071ea Other: Bump go-proton-api to v0.1.4 2022-12-01 08:19:16 +01:00
21 changed files with 230 additions and 126 deletions

View File

@ -2,6 +2,30 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.0.5] Perth Narrows
### Fixed
* GODT-2178: Bump go-proton-api to fix drafts.
* GODT-2180: Allow login with FIDO2.
## [Bridge 3.0.4] Perth Narrows
### Changed
* Other: Do not list \Deleted flag for All Mail.
* Other: Disable perma-delete for expunge on Spam folder.
### Fixed
* Other: Ensure expunge feature test pushes to error stack.
* GODT-2170: Use client-side draft update in integration tests.
* GODT-2170: Improving test server behaviour.
* GODT-2170: Update draft event means delete old and create new message.
* GODT-2170: User create draft route: first steps.
## [Bridge 3.0.3] Perth Narrows
### Fixed
* GPA v0.1.4: fix token expiration mechanism.
## [Bridge 3.0.2] Perth Narrows
### Changed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.0.1+git
BRIDGE_APP_VERSION?=3.0.5+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20221129150032-c663738a6cee
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.1.2
github.com/ProtonMail/go-proton-api v0.2.1
github.com/ProtonMail/go-rfc5322 v0.11.0
github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/PuerkitoBio/goquery v1.8.0

8
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20221129150032-c663738a6cee h1:rDGqVa4CepqpJF8TDjqnBITqD8OzrLzeg66ibVDCPSc=
github.com/ProtonMail/gluon v0.14.2-0.20221129150032-c663738a6cee/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c h1:DzVlJERHOHDQjYz/P12VlORS4rF2Ii83cWcYHsXGdng=
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
@ -43,8 +43,8 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NB
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.1.2 h1:MD0lbo8ohU1O+1mbMU6EkDmVj4BAq5e5cCPkIZgDF9Q=
github.com/ProtonMail/go-proton-api v0.1.2/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
github.com/ProtonMail/go-proton-api v0.2.1 h1:M15/zzfx6EPiskv2+gogUkmvx7Y1SmRRtLT6GiBh5T0=
github.com/ProtonMail/go-proton-api v0.2.1/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=

View File

@ -182,7 +182,7 @@ func (bridge *Bridge) LoginFull(
return "", fmt.Errorf("failed to begin login process: %w", err)
}
if auth.TwoFA.Enabled == proton.TOTPEnabled {
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP()

View File

@ -149,7 +149,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
return
}
if auth.TwoFA.Enabled == proton.TOTPEnabled {
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if code == "" {
f.printAndLogError("Cannot login: need two factor code")

View File

@ -406,7 +406,7 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt
s.auth = auth
switch {
case auth.TwoFA.Enabled == proton.TOTPEnabled:
case auth.TwoFA.Enabled&proton.HasTOTP != 0:
_ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username))
case auth.PasswordMode == proton.TwoPasswordMode:

View File

@ -389,6 +389,18 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}
case proton.EventUpdate, proton.EventUpdateFlags:
// Draft update means to completely remove old message and upload the new data again.
if event.Message.IsDraft() {
if err := user.handleUpdateDraftEvent(
logging.WithLogrusField(ctx, "action", "update draft"),
event,
); err != nil {
return fmt.Errorf("failed to handle update draft event: %w", err)
}
return nil
}
// GODT-2028 - Use better events here. It should be possible to have 3 separate events that refrain to
// whether the flags, labels or read only data (header+body) has been changed. This requires fixing proton
// first so that it correctly reports those cases.
@ -400,16 +412,6 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
return fmt.Errorf("failed to handle update message event: %w", err)
}
// Only issue body updates if the message is a draft.
if event.Message.IsDraft() {
if err := user.handleUpdateDraftEvent(
logging.WithLogrusField(ctx, "action", "update draft"),
event,
); err != nil {
return fmt.Errorf("failed to handle update draft event: %w", err)
}
}
case proton.EventDelete:
if err := user.handleDeleteMessageEvent(
logging.WithLogrusField(ctx, "action", "delete message"),

View File

@ -18,8 +18,10 @@
package user
import (
"bytes"
"context"
"fmt"
"net/mail"
"sync/atomic"
"time"
@ -31,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
"github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
@ -326,7 +329,7 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag
return err
}
if mailboxID == proton.SpamLabel || mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel {
if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel {
var metadata []proton.MessageMetadata
// There's currently no limit on how many IDs we can filter on,
@ -437,20 +440,37 @@ func (conn *imapConnector) importMessage(
if err := safe.RLockRet(func() error {
return withAddrKR(conn.apiUser, conn.apiAddrs[conn.addrID], conn.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
res, err := stream.Collect(ctx, conn.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{
AddressID: conn.addrID,
LabelIDs: labelIDs,
Unread: proton.Bool(unread),
Flags: flags,
},
Message: literal,
}}...))
if err != nil {
return fmt.Errorf("failed to import message: %w", err)
messageID := ""
if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID])
if err != nil {
return fmt.Errorf("failed to create draft: %w", err)
}
// apply labels
messageID = msg.ID
} else {
res, err := stream.Collect(ctx, conn.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{
AddressID: conn.addrID,
LabelIDs: labelIDs,
Unread: proton.Bool(unread),
Flags: flags,
},
Message: literal,
}}...))
if err != nil {
return fmt.Errorf("failed to import message: %w", err)
}
messageID = res[0].MessageID
}
if full, err = conn.client.GetFullMessage(ctx, res[0].MessageID); err != nil {
var err error
if full, err = conn.client.GetFullMessage(ctx, messageID); err != nil {
return fmt.Errorf("failed to fetch message: %w", err)
}
@ -497,6 +517,63 @@ func toIMAPMessage(message proton.MessageMetadata) imap.Message {
}
}
func (conn *imapConnector) createDraft(ctx context.Context, literal []byte, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) { //nolint:funlen
// Create a new message parser from the reader.
parser, err := parser.New(bytes.NewReader(literal))
if err != nil {
return proton.Message{}, fmt.Errorf("failed to create parser: %w", err)
}
message, err := message.ParseWithParser(parser)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)
}
decBody := string(message.PlainBody)
if message.RichBody != "" {
decBody = string(message.RichBody)
}
draft, err := conn.client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: message.Subject,
Body: decBody,
MIMEType: message.MIMEType,
Sender: &mail.Address{Name: sender.DisplayName, Address: sender.Email},
ToList: message.ToList,
CCList: message.CCList,
BCCList: message.BCCList,
ExternalID: message.ExternalID,
},
})
if err != nil {
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
}
for _, att := range message.Attachments {
disposition := proton.AttachmentDisposition
if att.Disposition == "inline" && att.ContentID != "" {
disposition = proton.InlineDisposition
}
if _, err := conn.client.UploadAttachment(ctx, addrKR, proton.CreateAttachmentReq{
MessageID: draft.ID,
Filename: att.Name,
MIMEType: rfc822.MIMEType(att.MIMEType),
Disposition: disposition,
ContentID: att.ContentID,
Body: att.Data,
}); err != nil {
return proton.Message{}, fmt.Errorf("failed to add attachment to draft: %w", err)
}
}
return draft, nil
}
func toIMAPMailbox(label proton.Label, flags, permFlags, attrs imap.FlagSet) imap.Mailbox {
if label.Type == proton.LabelTypeLabel {
label.Path = append([]string{labelPrefix}, label.Path...)

View File

@ -188,19 +188,9 @@ func sendWithKey( //nolint:funlen
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
}
encBody, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(decBody), nil)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to encrypt message body: %w", err)
}
armBody, err := encBody.GetArmored()
if err != nil {
return proton.Message{}, fmt.Errorf("failed to get armored message body: %w", err)
}
draft, err := createDraft(ctx, client, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
draft, err := createDraft(ctx, client, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
Subject: message.Subject,
Body: armBody,
Body: decBody,
MIMEType: message.MIMEType,
Sender: message.Sender,
@ -312,6 +302,7 @@ func getParentID( //nolint:funlen
func createDraft(
ctx context.Context,
client *proton.Client,
addrKR *crypto.KeyRing,
emails []string,
from string,
to []string,
@ -357,7 +348,7 @@ func createDraft(
action = proton.ForwardAction
}
return client.CreateDraft(ctx, proton.CreateDraftReq{
return client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: template,
ParentID: parentID,
Action: action,

View File

@ -333,6 +333,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
}
attrs := imap.NewFlagSet(imap.AttrNoInferiors)
permanentFlags := defaultPermanentFlags
flags := defaultFlags
switch labelID {
case proton.TrashLabel:
@ -343,6 +345,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
case proton.AllMailLabel:
attrs = attrs.Add(imap.AttrAll)
flags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged)
permanentFlags = imap.NewFlagSet(imap.FlagSeen, imap.FlagFlagged)
case proton.ArchiveLabel:
attrs = attrs.Add(imap.AttrArchive)
@ -360,8 +364,8 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
return imap.NewMailboxCreated(imap.Mailbox{
ID: labelID,
Name: []string{labelName},
Flags: defaultFlags,
PermanentFlags: defaultPermanentFlags,
Flags: flags,
PermanentFlags: permanentFlags,
Attributes: attrs,
})
}

View File

@ -19,7 +19,6 @@ package tests
import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
)
@ -36,8 +35,6 @@ type API interface {
RemoveAddress(userID, addrID string) error
RemoveAddressKey(userID, addrID, keyID string) error
UpdateDraft(userID, draftID string, changes proton.DraftTemplate) error
Close()
}

View File

@ -98,7 +98,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has the following messages in "([^"]*)":$`, s.theAddressOfAccountHasTheFollowingMessagesInMailbox)
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has (\d+) messages in "([^"]*)"$`, s.theAddressOfAccountHasMessagesInMailbox)
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has no keys$`, s.theAddressOfAccountHasNoKeys)
ctx.Step(`^the following fields where changed in draft (\d+) for address "([^"]*)" of account "([^"]*)":$`, s.addressDraftChanged)
ctx.Step(`^the following fields were changed in draft (\d+) for address "([^"]*)" of account "([^"]*)":$`, s.theFollowingFieldsWereChangedInDraftForAddressOfAccount)
// ==== BRIDGE ====
ctx.Step(`^bridge starts$`, s.bridgeStarts)

View File

@ -230,36 +230,28 @@ func (t *testCtx) getMBoxID(userID string, name string) string {
// getDraftID will return the API ID of draft message with draftIndex, where
// draftIndex is similar to sequential ID i.e. 1 represents the first message
// of draft folder sorted by API creation time.
func (t *testCtx) getDraftID(username string, draftIndex int) string {
if draftIndex < 1 {
panic(fmt.Sprintf("draft index suppose to be non-zero positive integer, but have %d", draftIndex))
}
func (t *testCtx) getDraftID(username string, draftIndex int) (string, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var draftID string
if err := t.withClient(ctx, username, func(ctx context.Context, client *proton.Client) error {
messages, err := client.GetMessageMetadata(
ctx, proton.MessageFilter{LabelID: proton.DraftsLabel},
)
messages, err := client.GetMessageMetadata(ctx, proton.MessageFilter{LabelID: proton.DraftsLabel})
if err != nil {
panic(err)
}
if len(messages) < draftIndex {
panic("draft index too high")
return fmt.Errorf("failed to get message metadata: %w", err)
} else if len(messages) < draftIndex {
return fmt.Errorf("draft index %d is out of range", draftIndex)
}
draftID = messages[draftIndex-1].ID
return nil
}); err != nil {
panic(err)
return "", err
}
return draftID
return draftID, nil
}
func (t *testCtx) getLastCall(method, pathExp string) (server.Call, error) {

View File

@ -27,6 +27,7 @@ Feature: IMAP create messages
And IMAP client "1" eventually sees the following messages in "Drafts":
| from | to | subject | body |
| user@pm.me | john.doe@email.com | foo | bar |
# This fails now
And IMAP client "1" eventually sees the following messages in "All Mail":
| from | to | subject | body |
| user@pm.me | john.doe@email.com | foo | bar |

View File

@ -16,12 +16,14 @@ Feature: IMAP remove messages from mailbox
And IMAP client "1" marks message 2 as deleted
Then IMAP client "1" sees that message 2 has the flag "\Deleted"
When IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 9 messages in "Folders/mbox"
Scenario: Mark all messages as deleted and EXPUNGE
When IMAP client "1" selects "Folders/mbox"
And IMAP client "1" marks all messages as deleted
And IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 0 messages in "Folders/mbox"
Scenario: Mark messages as undeleted and EXPUNGE
@ -30,11 +32,11 @@ Feature: IMAP remove messages from mailbox
But IMAP client "1" marks message 2 as not deleted
And IMAP client "1" marks message 3 as not deleted
When IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 2 messages in "Folders/mbox"
# TODO(GODT-1989): Re-enable!
# Scenario: Not possible to delete from All Mail and expunge does nothing
# When IMAP client "1" selects "All Mail"
# And IMAP client "1" marks message 2 as deleted
# And IMAP client "1" expunges
# Then IMAP client "1" eventually sees 10 messages in "All Mail"
Scenario: Not possible to delete from All Mail and expunge does nothing
When IMAP client "1" selects "All Mail"
And IMAP client "1" marks message 2 as deleted
And IMAP client "1" expunges
Then it fails

View File

@ -6,8 +6,8 @@ Feature: IMAP remove messages from Trash
| mbox | folder |
| label | label |
Scenario Outline: Message in Trash or Spam and some other label is not permanently deleted
Given the address "user@pm.me" of account "user@pm.me" has the following messages in "<mailbox>":
Scenario Outline: Message in Trash and some other label is not permanently deleted
Given the address "user@pm.me" of account "user@pm.me" has the following messages in "Trash":
| from | to | subject | body |
| john.doe@mail.com | user@pm.me | foo | hello |
| jane.doe@mail.com | name@pm.me | bar | world |
@ -15,27 +15,22 @@ Feature: IMAP remove messages from Trash
And the user logs in with username "user@pm.me" and password "password"
And user "user@pm.me" finishes syncing
And user "user@pm.me" connects and authenticates IMAP client "1"
And IMAP client "1" selects "<mailbox>"
When IMAP client "1" copies the message with subject "foo" from "<mailbox>" to "Labels/label"
And IMAP client "1" selects "Trash"
When IMAP client "1" copies the message with subject "foo" from "Trash" to "Labels/label"
Then it succeeds
When IMAP client "1" marks the message with subject "foo" as deleted
Then it succeeds
And IMAP client "1" sees 2 messages in "<mailbox>"
And IMAP client "1" sees 2 messages in "Trash"
And IMAP client "1" sees 2 messages in "All Mail"
And IMAP client "1" sees 1 messages in "Labels/label"
When IMAP client "1" expunges
Then it succeeds
And IMAP client "1" sees 1 messages in "<mailbox>"
And IMAP client "1" sees 1 messages in "Trash"
And IMAP client "1" sees 2 messages in "All Mail"
And IMAP client "1" sees 1 messages in "Labels/label"
Examples:
| mailbox |
| Spam |
| Trash |
Scenario Outline: Message in Trash or Spam only is permanently deleted
Given the address "user@pm.me" of account "user@pm.me" has the following messages in "<mailbox>":
Scenario Outline: Message in Trash only is permanently deleted
Given the address "user@pm.me" of account "user@pm.me" has the following messages in "Trash":
| from | to | subject | body |
| john.doe@mail.com | user@pm.me | foo | hello |
| jane.doe@mail.com | name@pm.me | bar | world |
@ -43,17 +38,12 @@ Feature: IMAP remove messages from Trash
And the user logs in with username "user@pm.me" and password "password"
And user "user@pm.me" finishes syncing
And user "user@pm.me" connects and authenticates IMAP client "1"
And IMAP client "1" selects "<mailbox>"
And IMAP client "1" selects "Trash"
When IMAP client "1" marks the message with subject "foo" as deleted
Then it succeeds
And IMAP client "1" sees 2 messages in "<mailbox>"
And IMAP client "1" sees 2 messages in "Trash"
And IMAP client "1" sees 2 messages in "All Mail"
When IMAP client "1" expunges
Then it succeeds
And IMAP client "1" sees 1 messages in "<mailbox>"
And IMAP client "1" eventually sees 1 messages in "All Mail"
Examples:
| mailbox |
| Spam |
| Trash |
And IMAP client "1" sees 1 messages in "Trash"
And IMAP client "1" eventually sees 1 messages in "All Mail"

View File

@ -11,10 +11,15 @@ Feature: IMAP Draft messages
This is a dra
"""
Then IMAP client "1" eventually sees the following messages in "Drafts":
| body |
| This is a dra |
And IMAP client "1" sees 1 messages in "Drafts"
Scenario: Draft edited locally
When IMAP client "1" marks message 1 as deleted
And IMAP client "1" expunges
And it succeeds
And IMAP client "1" appends the following message to "Drafts":
"""
Subject: Basic Draft
@ -30,7 +35,7 @@ Feature: IMAP Draft messages
And IMAP client "1" sees 1 messages in "Drafts"
Scenario: Draft edited remotely
When the following fields where changed in draft 1 for address "user@pm.me" of account "user@pm.me":
When the following fields were changed in draft 1 for address "user@pm.me" of account "user@pm.me":
| to | subject | body |
| someone@proton.me | Basic Draft | This is a draft body, but longer |
Then IMAP client "1" eventually sees the following messages in "Drafts":

View File

@ -33,6 +33,7 @@ import (
"github.com/emersion/go-imap"
id "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
@ -280,7 +281,9 @@ func (s *scenario) imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox
func (s *scenario) imapClientEventuallySeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error {
return eventually(func() error {
return s.imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox, table)
err := s.imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox, table)
logrus.WithError(err).Trace("Matching eventually")
return err
})
}
@ -374,7 +377,9 @@ func (s *scenario) imapClientSeesThatMessageHasTheFlag(clientID string, seq int,
func (s *scenario) imapClientExpunges(clientID string) error {
_, client := s.t.getIMAPClient(clientID)
return client.Expunge(nil)
s.t.pushError(client.Expunge(nil))
return nil
}
func (s *scenario) imapClientAppendsTheFollowingMessageToMailbox(clientID string, mailbox string, docString *godog.DocString) error {

View File

@ -144,7 +144,7 @@ func matchMessages(have, want []Message) error {
})
if !IsSub(ToAny(have), ToAny(want)) {
return fmt.Errorf("missing messages: have %+v, want %+v", have, want)
return fmt.Errorf("missing messages: have %#v, want %#v", have, want)
}
return nil

View File

@ -226,7 +226,7 @@ func (s *scenario) theAddressOfAccountHasNoKeys(address, username string) error
// accountDraftChanged changes the draft attributes, where draftIndex is
// similar to sequential ID i.e. 1 represents the first message of draft folder
// sorted by API creation time.
func (s *scenario) addressDraftChanged(draftIndex int, address, username string, table *godog.Table) error {
func (s *scenario) theFollowingFieldsWereChangedInDraftForAddressOfAccount(draftIndex int, address, username string, table *godog.Table) error {
wantMessages, err := unmarshalTable[Message](table)
if err != nil {
return err
@ -236,35 +236,49 @@ func (s *scenario) addressDraftChanged(draftIndex int, address, username string,
return fmt.Errorf("expected to have one row in table but got %d instead", len(wantMessages))
}
draftID := s.t.getDraftID(username, draftIndex)
encBody := []byte{}
if wantMessages[0].Body != "" {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := s.t.withClient(ctx, username, func(ctx context.Context, c *proton.Client) error {
return s.t.withAddrKR(ctx, c, username, s.t.getUserAddrID(s.t.getUserID(username), address),
func(ctx context.Context, addrKR *crypto.KeyRing) error {
var err error
encBody, err = proton.EncryptRFC822(addrKR, wantMessages[0].Build())
return err
})
}); err != nil {
return err
}
draftID, err := s.t.getDraftID(username, draftIndex)
if err != nil {
return fmt.Errorf("failed to get draft ID: %w", err)
}
changes := proton.DraftTemplate{
Subject: wantMessages[0].Subject,
Body: string(encBody),
}
if wantMessages[0].To != "" {
changes.ToList = []*mail.Address{{Address: wantMessages[0].To}}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
return s.t.api.UpdateDraft(s.t.getUserID(username), draftID, changes)
return s.t.withClient(ctx, username, func(ctx context.Context, c *proton.Client) error {
return s.t.withAddrKR(ctx, c, username, s.t.getUserAddrID(s.t.getUserID(username), address), func(_ context.Context, addrKR *crypto.KeyRing) error {
var changes proton.DraftTemplate
if wantMessages[0].From != "" {
return fmt.Errorf("changing from address is not supported")
}
if wantMessages[0].To != "" {
changes.ToList = []*mail.Address{{Address: wantMessages[0].To}}
}
if wantMessages[0].CC != "" {
changes.CCList = []*mail.Address{{Address: wantMessages[0].CC}}
}
if wantMessages[0].BCC != "" {
changes.BCCList = []*mail.Address{{Address: wantMessages[0].BCC}}
}
if wantMessages[0].Subject != "" {
changes.Subject = wantMessages[0].Subject
}
if wantMessages[0].Body != "" {
changes.Body = wantMessages[0].Body
}
if _, err := c.UpdateDraft(ctx, draftID, addrKR, proton.UpdateDraftReq{Message: changes}); err != nil {
return fmt.Errorf("failed to update draft: %w", err)
}
return nil
})
})
}
func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error {