forked from Silverfish/proton-bridge
GODT-1954: Draft message support
Add special case handling for draft messages so that if a Draft is updated via an event it is correctly updated on the IMAP client via a the new `imap.MessageUpdated event`. This patch also updates Gluon to the latest version.
This commit is contained in:
@ -19,6 +19,7 @@ package tests
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
"gitlab.protontech.ch/go/liteapi/server"
|
||||
)
|
||||
|
||||
@ -36,6 +37,8 @@ type API interface {
|
||||
GetAddressKeyIDs(userID, addrID string) ([]string, error)
|
||||
RemoveAddressKey(userID, addrID, keyID string) error
|
||||
|
||||
UpdateDraft(userID, draftID string, changes liteapi.DraftTemplate) error
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -57,6 +58,11 @@ func (s *scenario) close(_ testing.TB) {
|
||||
}
|
||||
|
||||
func TestFeatures(testingT *testing.T) {
|
||||
paths := []string{"features"}
|
||||
if features := os.Getenv("FEATURES"); features != "" {
|
||||
paths = strings.Split(features, " ")
|
||||
}
|
||||
|
||||
suite := godog.TestSuite{
|
||||
ScenarioInitializer: func(ctx *godog.ScenarioContext) {
|
||||
var s scenario
|
||||
@ -98,6 +104,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)
|
||||
|
||||
// ==== BRIDGE ====
|
||||
ctx.Step(`^bridge starts$`, s.bridgeStarts)
|
||||
@ -191,7 +198,7 @@ func TestFeatures(testingT *testing.T) {
|
||||
},
|
||||
Options: &godog.Options{
|
||||
Format: "pretty",
|
||||
Paths: []string{"features"},
|
||||
Paths: paths,
|
||||
TestingT: testingT,
|
||||
},
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
)
|
||||
@ -48,43 +49,54 @@ func (t *testCtx) withClient(ctx context.Context, username string, fn func(conte
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testCtx) withAddrKR(
|
||||
ctx context.Context,
|
||||
c *liteapi.Client,
|
||||
username, addrID string,
|
||||
fn func(context.Context, *crypto.KeyRing) error,
|
||||
) error {
|
||||
user, err := c.GetUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := c.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
salt, err := c.GetSalts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPass, err := salt.SaltForKey([]byte(t.getUserPass(t.getUserID(username))), user.Keys.Primary().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, addrKRs, err := liteapi.Unlock(user, addr, keyPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fn(ctx, addrKRs[addrID])
|
||||
}
|
||||
|
||||
func (t *testCtx) createMessages(ctx context.Context, username, addrID string, req []liteapi.ImportReq) error {
|
||||
return t.withClient(ctx, username, func(ctx context.Context, c *liteapi.Client) error {
|
||||
user, err := c.GetUser(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.withAddrKR(ctx, c, username, addrID, func(ctx context.Context, addrKR *crypto.KeyRing) error {
|
||||
if _, err := stream.Collect(ctx, c.ImportMessages(
|
||||
ctx,
|
||||
addrKR,
|
||||
runtime.NumCPU(),
|
||||
runtime.NumCPU(),
|
||||
req...,
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, err := c.GetAddresses(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
salt, err := c.GetSalts(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPass, err := salt.SaltForKey([]byte(t.getUserPass(t.getUserID(username))), user.Keys.Primary().ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, addrKRs, err := liteapi.Unlock(user, addr, keyPass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := stream.Collect(ctx, c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
runtime.NumCPU(),
|
||||
req...,
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -216,6 +216,41 @@ func (t *testCtx) getMBoxID(userID string, name string) string {
|
||||
return labelID
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var draftID string
|
||||
|
||||
if err := t.withClient(ctx, username, func(ctx context.Context, client *liteapi.Client) error {
|
||||
messages, err := client.GetMessageMetadata(
|
||||
ctx, liteapi.MessageFilter{LabelID: liteapi.DraftsLabel},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(messages) < draftIndex {
|
||||
panic("draft index too high")
|
||||
}
|
||||
|
||||
draftID = messages[draftIndex-1].ID
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return draftID
|
||||
}
|
||||
|
||||
func (t *testCtx) getLastCall(method, pathExp string) (server.Call, error) {
|
||||
t.callsLock.RLock()
|
||||
defer t.callsLock.RUnlock()
|
||||
|
||||
@ -24,10 +24,10 @@ Feature: IMAP create messages
|
||||
| from | to | subject | body |
|
||||
| user@pm.me | john.doe@email.com | foo | bar |
|
||||
Then it succeeds
|
||||
And IMAP client "1" sees the following messages in "Drafts":
|
||||
And IMAP client "1" eventually sees the following messages in "Drafts":
|
||||
| from | to | subject | body |
|
||||
| user@pm.me | john.doe@email.com | foo | bar |
|
||||
And IMAP client "1" sees the following messages in "All Mail":
|
||||
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 |
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ Feature: IMAP remove messages from Trash
|
||||
| label | label |
|
||||
|
||||
Scenario Outline: Message in Trash or Spam and some other label is not permanently deleted
|
||||
And 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 "<mailbox>":
|
||||
| from | to | subject | body |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world |
|
||||
@ -35,7 +35,7 @@ Feature: IMAP remove messages from Trash
|
||||
| Trash |
|
||||
|
||||
Scenario Outline: Message in Trash or Spam only is permanently deleted
|
||||
And 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 "<mailbox>":
|
||||
| from | to | subject | body |
|
||||
| john.doe@mail.com | user@pm.me | foo | hello |
|
||||
| jane.doe@mail.com | name@pm.me | bar | world |
|
||||
|
||||
40
tests/features/imap/message/drafts.feature
Normal file
40
tests/features/imap/message/drafts.feature
Normal file
@ -0,0 +1,40 @@
|
||||
Feature: IMAP Draft messages
|
||||
Background:
|
||||
Given there exists an account with username "user@pm.me" and password "password"
|
||||
And bridge starts
|
||||
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 "Drafts"
|
||||
When IMAP client "1" appends the following message to "Drafts":
|
||||
"""
|
||||
|
||||
This is a dra
|
||||
"""
|
||||
|
||||
Scenario: Draft edited locally
|
||||
When IMAP client "1" marks message 1 as deleted
|
||||
And IMAP client "1" expunges
|
||||
And IMAP client "1" appends the following message to "Drafts":
|
||||
"""
|
||||
Subject: Basic Draft
|
||||
Content-Type: text/plain
|
||||
To: someone@proton.me
|
||||
|
||||
This is a draft, but longer
|
||||
"""
|
||||
Then it succeeds
|
||||
And IMAP client "1" eventually sees the following messages in "Drafts":
|
||||
| to | subject | body |
|
||||
| someone@proton.me | Basic Draft | This is a draft, but longer |
|
||||
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":
|
||||
| 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":
|
||||
| to | subject | body |
|
||||
| someone@proton.me | Basic Draft | This is a draft body, but longer |
|
||||
And IMAP client "1" sees 1 messages in "Drafts"
|
||||
|
||||
@ -21,7 +21,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/cucumber/godog"
|
||||
@ -218,6 +220,50 @@ func (s *scenario) theAddressOfAccountHasNoKeys(address, username string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
wantMessages, err := unmarshalTable[Message](table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(wantMessages) != 1 {
|
||||
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 *liteapi.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 = liteapi.EncryptRFC822(addrKR, wantMessages[0].Build())
|
||||
return err
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
changes := liteapi.DraftTemplate{
|
||||
Subject: wantMessages[0].Subject,
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *scenario) userLogsInWithUsernameAndPassword(username, password string) error {
|
||||
userID, err := s.t.bridge.LoginFull(context.Background(), username, []byte(password), nil, nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user