fix(GODT-2578): Refresh literals appended to Sent folder

Whenever a message gets moved to the sent folder we should retrieve the
new literal in order to guarantee that, if another client modifies and
sends the message, we always see the latest version of the message and
not a previous state stored in the Gluon cache.

Includes the following Gluon MRs:
* https://github.com/ProtonMail/gluon/pull/374
* https://github.com/ProtonMail/gluon/pull/376

Includes the followin gpa MR:
https://github.com/ProtonMail/go-proton-api/pull/88
This commit is contained in:
Leander Beernaert
2023-06-30 13:02:38 +02:00
parent b2eb35592f
commit cc17366c1c
5 changed files with 200 additions and 18 deletions

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.16.1-0.20230628130101-52391e3cbf12
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184
github.com/ProtonMail/go-proton-api v0.4.1-0.20230704060229-a77a437ec052
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible

13
go.sum
View File

@ -23,12 +23,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.16.1-0.20230628083422-e2cfbe8c5823 h1:mOaTSgPEUu0KZjH1SN7ZxqWIljlZFvyST48coZlvC1o=
github.com/ProtonMail/gluon v0.16.1-0.20230628083422-e2cfbe8c5823/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.16.1-0.20230628112102-80c05f878b53 h1:38aw1TGmZlmY8l7uo+RRHnm97nZJz8xkEG//yPUS+9k=
github.com/ProtonMail/gluon v0.16.1-0.20230628112102-80c05f878b53/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.16.1-0.20230628130101-52391e3cbf12 h1:/3S2RdUUASLW/JD+17yeJ+tanU6yWD4yzQSEXdilMMk=
github.com/ProtonMail/gluon v0.16.1-0.20230628130101-52391e3cbf12/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de h1:th289W4w6aE6IAdHwZgfHIgtK1ew9ZvAdXnie0BtVuw=
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
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-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -39,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184 h1:gw8sgQMCIDS/lw5xbF2iqlTfvY0HhuafjlGsKcN3VsE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230628092916-81cb3f87f184/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230704060229-a77a437ec052 h1:uIq0RX4gU9PSZ9x5b2LmJUXNOuBXRRVSOkM1RGnSy68=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230704060229-a77a437ec052/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
@ -270,7 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=

View File

@ -0,0 +1,175 @@
// Copyright (c) 2023 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 bridge_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
"testing"
"time"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
go_imap "github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
getGluonHeaderID := func(literal []byte) (string, string) {
h, err := rfc822.NewHeader(literal)
require.NoError(t, err)
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
require.True(t, ok)
externalID, ok := h.GetChecked("Message-Id")
require.True(t, ok)
return gluonID, externalID
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("imap", password)
require.NoError(t, err)
_, _, err = s.CreateUser("bar", password)
require.NoError(t, err)
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
waiter := waitForIMAPServerReady(b)
defer waiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
waiter.Wait()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Create first draft in client.
literal := fmt.Sprintf(`From: %v
To: %v
Date: Fri, 3 Feb 2023 01:04:32 +0100
Subject: Foo
Hello
`, info.Addresses[0], "bar@proton.local")
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
// Verify the draft is available in client.
require.Eventually(t, func() bool {
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
require.NoError(t, err)
return status.Messages == 1
}, 2*time.Second, time.Second)
// Retrieve the new literal so we can have the Proton Message ID.
messages, err := clientFetch(client, "Drafts")
require.NoError(t, err)
require.Equal(t, 1, len(messages))
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
logrus.Info(string(newLiteral))
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
// Modify new literal.
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
func() {
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err)
defer func() { _ = smtpClient.Close() }()
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
info.Addresses[0],
info.Addresses[0],
string(info.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
info.Addresses[0],
[]string{"bar@proton.local"},
bytes.NewReader(newLiteralModified),
))
}()
// Append message to Sent as the imap client would.
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
// Verify the sent message gets updated with the new literal.
require.Eventually(t, func() bool {
// Check if sent message matches the latest draft.
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
require.NoError(t, err)
if len(messagesClient1) != 1 {
return false
}
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
require.NoError(t, err)
sentLiteralStr := string(sentLiteralText)
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
idIsDifferent := sentLiteralID != newLiteralID
externIDMatches := sentLiteralExternID == newLiteralExternID
return literalMatches && idIsDifferent && externIDMatches
}, 2*time.Second, time.Second)
})
}, server.WithMessageDedup())
}

View File

@ -399,6 +399,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
}
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
}
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
user, err := c.GetUser(ctx)
require.NoError(t, err)
@ -417,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
_, ok := addrKRs[addrID]
require.True(t, ok)
var msgFlags proton.MessageFlag
if flags == 0 {
msgFlags = proton.MessageFlagReceived
} else {
msgFlags = flags
}
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
@ -427,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
Metadata: proton.ImportMetadata{
AddressID: addrID,
LabelIDs: []string{labelID},
Flags: proton.MessageFlagReceived,
Flags: msgFlags,
},
Message: message,
}

View File

@ -514,9 +514,9 @@ 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, but we should
// only do this if the event is of type EventUpdate otherwise label switch operations will not work.
if event.Message.IsDraft() && event.Action == proton.EventUpdate {
updates, err := user.handleUpdateDraftEvent(
logging.WithLogrusField(ctx, "action", "update draft"),
if (event.Message.IsDraft() || (event.Message.Flags&proton.MessageFlagSent != 0)) && event.Action == proton.EventUpdate {
updates, err := user.handleUpdateDraftOrSentMessage(
logging.WithLogrusField(ctx, "action", "update draft or sent message"),
event,
)
if err != nil {
@ -701,18 +701,19 @@ func (user *User) handleDeleteMessageEvent(_ context.Context, event proton.Messa
}, user.updateChLock)
}
func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) {
func (user *User) handleUpdateDraftOrSentMessage(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) {
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": event.ID,
"subject": logging.Sensitive(event.Message.Subject),
}).Info("Handling draft updated event")
"isDraft": event.Message.IsDraft(),
}).Info("Handling draft or sent updated event")
full, err := user.client.GetFullMessage(ctx, event.Message.ID, newProtonAPIScheduler(user.panicHandler), proton.NewDefaultAttachmentAllocator())
if err != nil {
// If the message is not found, it means that it has been deleted before we could fetch it.
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status == http.StatusUnprocessableEntity {
user.log.WithField("messageID", event.Message.ID).Warn("Cannot update draft: full message is missing on API")
user.log.WithField("messageID", event.Message.ID).Warn("Cannot update message: full message is missing on API")
return nil, nil
}