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

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())
}