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/) 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 ## [Bridge 3.0.2] Perth Narrows
### Changed ### 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 .PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository. # 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_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG APP_VENDOR:=Proton AG

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.18
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1 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-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/go-rfc5322 v0.11.0
github.com/ProtonMail/gopenpgp/v2 v2.4.10 github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/PuerkitoBio/goquery v1.8.0 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/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 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= 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.20221202093012-ad1570c49c8c h1:DzVlJERHOHDQjYz/P12VlORS4rF2Ii83cWcYHsXGdng=
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/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 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-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= 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-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 h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM= 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.2.1 h1:M15/zzfx6EPiskv2+gogUkmvx7Y1SmRRtLT6GiBh5T0=
github.com/ProtonMail/go-proton-api v0.1.2/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk= 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 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw= github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= 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) 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") logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP() totp, err := getTOTP()

View File

@ -149,7 +149,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
return return
} }
if auth.TwoFA.Enabled == proton.TOTPEnabled { if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty) code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if code == "" { if code == "" {
f.printAndLogError("Cannot login: need two factor 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 s.auth = auth
switch { switch {
case auth.TwoFA.Enabled == proton.TOTPEnabled: case auth.TwoFA.Enabled&proton.HasTOTP != 0:
_ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username)) _ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username))
case auth.PasswordMode == proton.TwoPasswordMode: 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: 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 // 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 // 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. // 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) 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: case proton.EventDelete:
if err := user.handleDeleteMessageEvent( if err := user.handleDeleteMessageEvent(
logging.WithLogrusField(ctx, "action", "delete message"), logging.WithLogrusField(ctx, "action", "delete message"),

View File

@ -18,8 +18,10 @@
package user package user
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"net/mail"
"sync/atomic" "sync/atomic"
"time" "time"
@ -31,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/message" "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/stream"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -326,7 +329,7 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag
return err return err
} }
if mailboxID == proton.SpamLabel || mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel { if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel {
var metadata []proton.MessageMetadata var metadata []proton.MessageMetadata
// There's currently no limit on how many IDs we can filter on, // 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 { if err := safe.RLockRet(func() error {
return withAddrKR(conn.apiUser, conn.apiAddrs[conn.addrID], conn.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) 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{{ messageID := ""
Metadata: proton.ImportMetadata{
AddressID: conn.addrID, if slices.Contains(labelIDs, proton.DraftsLabel) {
LabelIDs: labelIDs, msg, err := conn.createDraft(ctx, literal, addrKR, conn.apiAddrs[conn.addrID])
Unread: proton.Bool(unread), if err != nil {
Flags: flags, return fmt.Errorf("failed to create draft: %w", err)
}, }
Message: literal,
}}...)) // apply labels
if err != nil {
return fmt.Errorf("failed to import message: %w", err) 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) 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 { func toIMAPMailbox(label proton.Label, flags, permFlags, attrs imap.FlagSet) imap.Mailbox {
if label.Type == proton.LabelTypeLabel { if label.Type == proton.LabelTypeLabel {
label.Path = append([]string{labelPrefix}, label.Path...) 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) return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
} }
encBody, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(decBody), nil) draft, err := createDraft(ctx, client, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
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{
Subject: message.Subject, Subject: message.Subject,
Body: armBody, Body: decBody,
MIMEType: message.MIMEType, MIMEType: message.MIMEType,
Sender: message.Sender, Sender: message.Sender,
@ -312,6 +302,7 @@ func getParentID( //nolint:funlen
func createDraft( func createDraft(
ctx context.Context, ctx context.Context,
client *proton.Client, client *proton.Client,
addrKR *crypto.KeyRing,
emails []string, emails []string,
from string, from string,
to []string, to []string,
@ -357,7 +348,7 @@ func createDraft(
action = proton.ForwardAction action = proton.ForwardAction
} }
return client.CreateDraft(ctx, proton.CreateDraftReq{ return client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: template, Message: template,
ParentID: parentID, ParentID: parentID,
Action: action, Action: action,

View File

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

View File

@ -19,7 +19,6 @@ package tests
import ( import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server" "github.com/ProtonMail/go-proton-api/server"
) )
@ -36,8 +35,6 @@ type API interface {
RemoveAddress(userID, addrID string) error RemoveAddress(userID, addrID string) error
RemoveAddressKey(userID, addrID, keyID string) error RemoveAddressKey(userID, addrID, keyID string) error
UpdateDraft(userID, draftID string, changes proton.DraftTemplate) error
Close() 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 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 (\d+) messages in "([^"]*)"$`, s.theAddressOfAccountHasMessagesInMailbox)
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has no keys$`, s.theAddressOfAccountHasNoKeys) 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 ==== // ==== BRIDGE ====
ctx.Step(`^bridge starts$`, s.bridgeStarts) 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 // 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 // draftIndex is similar to sequential ID i.e. 1 represents the first message
// of draft folder sorted by API creation time. // of draft folder sorted by API creation time.
func (t *testCtx) getDraftID(username string, draftIndex int) string { func (t *testCtx) getDraftID(username string, draftIndex int) (string, error) {
if draftIndex < 1 {
panic(fmt.Sprintf("draft index suppose to be non-zero positive integer, but have %d", draftIndex))
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
var draftID string var draftID string
if err := t.withClient(ctx, username, func(ctx context.Context, client *proton.Client) error { if err := t.withClient(ctx, username, func(ctx context.Context, client *proton.Client) error {
messages, err := client.GetMessageMetadata( messages, err := client.GetMessageMetadata(ctx, proton.MessageFilter{LabelID: proton.DraftsLabel})
ctx, proton.MessageFilter{LabelID: proton.DraftsLabel},
)
if err != nil { if err != nil {
panic(err) 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)
if len(messages) < draftIndex {
panic("draft index too high")
} }
draftID = messages[draftIndex-1].ID draftID = messages[draftIndex-1].ID
return nil return nil
}); err != nil { }); err != nil {
panic(err) return "", err
} }
return draftID return draftID, nil
} }
func (t *testCtx) getLastCall(method, pathExp string) (server.Call, error) { 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": And IMAP client "1" eventually sees the following messages in "Drafts":
| from | to | subject | body | | from | to | subject | body |
| user@pm.me | john.doe@email.com | foo | bar | | user@pm.me | john.doe@email.com | foo | bar |
# This fails now
And IMAP client "1" eventually sees the following messages in "All Mail": And IMAP client "1" eventually sees the following messages in "All Mail":
| from | to | subject | body | | from | to | subject | body |
| user@pm.me | john.doe@email.com | foo | bar | | 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 And IMAP client "1" marks message 2 as deleted
Then IMAP client "1" sees that message 2 has the flag "\Deleted" Then IMAP client "1" sees that message 2 has the flag "\Deleted"
When IMAP client "1" expunges When IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 9 messages in "Folders/mbox" Then IMAP client "1" sees 9 messages in "Folders/mbox"
Scenario: Mark all messages as deleted and EXPUNGE Scenario: Mark all messages as deleted and EXPUNGE
When IMAP client "1" selects "Folders/mbox" When IMAP client "1" selects "Folders/mbox"
And IMAP client "1" marks all messages as deleted And IMAP client "1" marks all messages as deleted
And IMAP client "1" expunges And IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 0 messages in "Folders/mbox" Then IMAP client "1" sees 0 messages in "Folders/mbox"
Scenario: Mark messages as undeleted and EXPUNGE 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 But IMAP client "1" marks message 2 as not deleted
And IMAP client "1" marks message 3 as not deleted And IMAP client "1" marks message 3 as not deleted
When IMAP client "1" expunges When IMAP client "1" expunges
And it succeeds
Then IMAP client "1" sees 2 messages in "Folders/mbox" 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
# Scenario: Not possible to delete from All Mail and expunge does nothing When IMAP client "1" selects "All Mail"
# When IMAP client "1" selects "All Mail" And IMAP client "1" marks message 2 as deleted
# And IMAP client "1" marks message 2 as deleted And IMAP client "1" expunges
# And IMAP client "1" expunges Then it fails
# Then IMAP client "1" eventually sees 10 messages in "All Mail"

View File

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

View File

@ -11,10 +11,15 @@ Feature: IMAP Draft messages
This is a dra 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 Scenario: Draft edited locally
When IMAP client "1" marks message 1 as deleted When IMAP client "1" marks message 1 as deleted
And IMAP client "1" expunges And IMAP client "1" expunges
And it succeeds
And IMAP client "1" appends the following message to "Drafts": And IMAP client "1" appends the following message to "Drafts":
""" """
Subject: Basic Draft Subject: Basic Draft
@ -30,7 +35,7 @@ Feature: IMAP Draft messages
And IMAP client "1" sees 1 messages in "Drafts" And IMAP client "1" sees 1 messages in "Drafts"
Scenario: Draft edited remotely 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 | | to | subject | body |
| someone@proton.me | Basic Draft | This is a draft body, but longer | | someone@proton.me | Basic Draft | This is a draft body, but longer |
Then IMAP client "1" eventually sees the following messages in "Drafts": Then IMAP client "1" eventually sees the following messages in "Drafts":

View File

@ -33,6 +33,7 @@ import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
id "github.com/emersion/go-imap-id" id "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices" "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 { func (s *scenario) imapClientEventuallySeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error {
return eventually(func() 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 { func (s *scenario) imapClientExpunges(clientID string) error {
_, client := s.t.getIMAPClient(clientID) _, 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 { 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)) { 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 return nil

View File

@ -226,7 +226,7 @@ func (s *scenario) theAddressOfAccountHasNoKeys(address, username string) error
// accountDraftChanged changes the draft attributes, where draftIndex is // accountDraftChanged changes the draft attributes, where draftIndex is
// similar to sequential ID i.e. 1 represents the first message of draft folder // similar to sequential ID i.e. 1 represents the first message of draft folder
// sorted by API creation time. // 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) wantMessages, err := unmarshalTable[Message](table)
if err != nil { if err != nil {
return err 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)) return fmt.Errorf("expected to have one row in table but got %d instead", len(wantMessages))
} }
draftID := s.t.getDraftID(username, draftIndex) draftID, err := s.t.getDraftID(username, draftIndex)
if err != nil {
encBody := []byte{} return fmt.Errorf("failed to get draft ID: %w", err)
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
}
} }
changes := proton.DraftTemplate{ ctx, cancel := context.WithCancel(context.Background())
Subject: wantMessages[0].Subject, defer cancel()
Body: string(encBody),
}
if wantMessages[0].To != "" {
changes.ToList = []*mail.Address{{Address: wantMessages[0].To}}
}
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 { func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error {