forked from Silverfish/proton-bridge
chore: Merge remote-tracking branch 'origin/devel' into release/trift
This commit is contained in:
4
go.mod
4
go.mod
@ -5,9 +5,9 @@ go 1.20
|
|||||||
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.2.0
|
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-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/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
|
|||||||
13
go.sum
13
go.sum
@ -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/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.16.1-0.20230628083422-e2cfbe8c5823 h1:mOaTSgPEUu0KZjH1SN7ZxqWIljlZFvyST48coZlvC1o=
|
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de h1:th289W4w6aE6IAdHwZgfHIgtK1ew9ZvAdXnie0BtVuw=
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230628083422-e2cfbe8c5823/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.16.1-0.20230704083024-d901d16834de/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/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-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
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-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 h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
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.20230704060229-a77a437ec052 h1:uIq0RX4gU9PSZ9x5b2LmJUXNOuBXRRVSOkM1RGnSy68=
|
||||||
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/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
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/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
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/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.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 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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
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=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import (
|
|||||||
func (bridge *Bridge) ReportBugClicked() {
|
func (bridge *Bridge) ReportBugClicked() {
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugSent()
|
user.ReportBugClicked()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|||||||
175
internal/bridge/draft_test.go
Normal file
175
internal/bridge/draft_test.go
Normal 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())
|
||||||
|
}
|
||||||
@ -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 {
|
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)
|
user, err := c.GetUser(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -417,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
_, ok := addrKRs[addrID]
|
_, ok := addrKRs[addrID]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
|
var msgFlags proton.MessageFlag
|
||||||
|
if flags == 0 {
|
||||||
|
msgFlags = proton.MessageFlagReceived
|
||||||
|
} else {
|
||||||
|
msgFlags = flags
|
||||||
|
}
|
||||||
|
|
||||||
str, err := c.ImportMessages(
|
str, err := c.ImportMessages(
|
||||||
ctx,
|
ctx,
|
||||||
addrKRs[addrID],
|
addrKRs[addrID],
|
||||||
@ -427,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
Metadata: proton.ImportMetadata{
|
Metadata: proton.ImportMetadata{
|
||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
LabelIDs: []string{labelID},
|
LabelIDs: []string{labelID},
|
||||||
Flags: proton.MessageFlagReceived,
|
Flags: msgFlags,
|
||||||
},
|
},
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
@ -74,8 +75,9 @@ func (status *ConfigurationStatus) Save() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
err = json.NewEncoder(f).Encode(status.Data)
|
enc.SetIndent("", " ")
|
||||||
|
err = enc.Encode(status.Data)
|
||||||
if err := f.Close(); err != nil {
|
if err := f.Close(); err != nil {
|
||||||
logrus.WithError(err).Error("Error while closing configstatus file.")
|
logrus.WithError(err).Error("Error while closing configstatus file.")
|
||||||
}
|
}
|
||||||
@ -197,3 +199,23 @@ func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
|
|||||||
val := data.DataV1.ClickedLink & (1 << pos)
|
val := data.DataV1.ClickedLink & (1 << pos)
|
||||||
return val > 0
|
return val > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (data *ConfigurationStatusData) clickedLinkToString() string {
|
||||||
|
var str = ""
|
||||||
|
var first = true
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
if data.hasLinkClicked(uint(i)) {
|
||||||
|
if !first {
|
||||||
|
str += ","
|
||||||
|
} else {
|
||||||
|
first = false
|
||||||
|
str += "["
|
||||||
|
}
|
||||||
|
str += strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if str != "" {
|
||||||
|
str += "]"
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|||||||
@ -17,16 +17,19 @@
|
|||||||
|
|
||||||
package configstatus
|
package configstatus
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ConfigAbortValues struct {
|
type ConfigAbortValues struct {
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigAbortDimensions struct {
|
type ConfigAbortDimensions struct {
|
||||||
ReportClick interface{} `json:"report_click"`
|
ReportClick string `json:"report_click"`
|
||||||
ReportSent interface{} `json:"report_sent"`
|
ReportSent string `json:"report_sent"`
|
||||||
ClickedLink uint64 `json:"clicked_link"`
|
ClickedLink string `json:"clicked_link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigAbortData struct {
|
type ConfigAbortData struct {
|
||||||
@ -46,9 +49,9 @@ func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
|||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
ReportClick: data.DataV1.ReportClick,
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: data.DataV1.ReportSent,
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: data.DataV1.ClickedLink,
|
ClickedLink: data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,9 +38,9 @@ func TestConfigurationAbort_default(t *testing.T) {
|
|||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
require.Equal(t, 0, req.Values.Duration)
|
require.Equal(t, 0, req.Values.Duration)
|
||||||
require.Equal(t, false, req.Dimensions.ReportClick)
|
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(0), req.Dimensions.ClickedLink)
|
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigurationAbort_fed(t *testing.T) {
|
func TestConfigurationAbort_fed(t *testing.T) {
|
||||||
@ -69,7 +69,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
|||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
require.Equal(t, 10, req.Values.Duration)
|
require.Equal(t, 10, req.Values.Duration)
|
||||||
require.Equal(t, true, req.Dimensions.ReportClick)
|
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(42), req.Dimensions.ClickedLink)
|
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressD
|
|||||||
|
|
||||||
func numberOfDay(now, prev time.Time) int {
|
func numberOfDay(now, prev time.Time) int {
|
||||||
if now.IsZero() || prev.IsZero() {
|
if now.IsZero() || prev.IsZero() {
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
if now.Year() > prev.Year() {
|
if now.Year() > prev.Year() {
|
||||||
if now.YearDay() > prev.YearDay() {
|
if now.YearDay() > prev.YearDay() {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
|||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
require.Equal(t, 0, req.Values.NbDay)
|
require.Equal(t, 0, req.Values.NbDay)
|
||||||
require.Equal(t, 0, req.Values.NbDaySinceLast)
|
require.Equal(t, 1, req.Values.NbDaySinceLast)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigurationProgress_fed(t *testing.T) {
|
func TestConfigurationProgress_fed(t *testing.T) {
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package configstatus
|
package configstatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,11 +27,11 @@ type ConfigRecoveryValues struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigRecoveryDimensions struct {
|
type ConfigRecoveryDimensions struct {
|
||||||
Autoconf string `json:"autoconf"`
|
Autoconf string `json:"autoconf"`
|
||||||
ReportClick interface{} `json:"report_click"`
|
ReportClick string `json:"report_click"`
|
||||||
ReportSent interface{} `json:"report_sent"`
|
ReportSent string `json:"report_sent"`
|
||||||
ClickedLink uint64 `json:"clicked_link"`
|
ClickedLink string `json:"clicked_link"`
|
||||||
FailureDetails string `json:"failure_details"`
|
FailureDetails string `json:"failure_details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigRecoveryData struct {
|
type ConfigRecoveryData struct {
|
||||||
@ -51,9 +52,9 @@ func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryD
|
|||||||
},
|
},
|
||||||
Dimensions: ConfigRecoveryDimensions{
|
Dimensions: ConfigRecoveryDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: data.DataV1.Autoconf,
|
||||||
ReportClick: data.DataV1.ReportClick,
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: data.DataV1.ReportSent,
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: data.DataV1.ClickedLink,
|
ClickedLink: data.clickedLinkToString(),
|
||||||
FailureDetails: data.DataV1.FailureDetails,
|
FailureDetails: data.DataV1.FailureDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,9 +39,9 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
|||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
require.Equal(t, 0, req.Values.Duration)
|
require.Equal(t, 0, req.Values.Duration)
|
||||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||||
require.Equal(t, false, req.Dimensions.ReportClick)
|
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(0), req.Dimensions.ClickedLink)
|
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||||
require.Equal(t, "", req.Dimensions.FailureDetails)
|
require.Equal(t, "", req.Dimensions.FailureDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +72,8 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
|||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
require.Equal(t, 10, req.Values.Duration)
|
require.Equal(t, 10, req.Values.Duration)
|
||||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||||
require.Equal(t, true, req.Dimensions.ReportClick)
|
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(42), req.Dimensions.ClickedLink)
|
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||||
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
|
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package configstatus
|
package configstatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,10 +27,10 @@ type ConfigSuccessValues struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigSuccessDimensions struct {
|
type ConfigSuccessDimensions struct {
|
||||||
Autoconf string `json:"autoconf"`
|
Autoconf string `json:"autoconf"`
|
||||||
ReportClick interface{} `json:"report_click"`
|
ReportClick string `json:"report_click"`
|
||||||
ReportSent interface{} `json:"report_sent"`
|
ReportSent string `json:"report_sent"`
|
||||||
ClickedLink uint64 `json:"clicked_link"`
|
ClickedLink string `json:"clicked_link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigSuccessData struct {
|
type ConfigSuccessData struct {
|
||||||
@ -50,9 +51,9 @@ func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessDat
|
|||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: data.DataV1.Autoconf,
|
||||||
ReportClick: data.DataV1.ReportClick,
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: data.DataV1.ReportSent,
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: data.DataV1.ClickedLink,
|
ClickedLink: data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,9 +39,9 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
|||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
require.Equal(t, 0, req.Values.Duration)
|
require.Equal(t, 0, req.Values.Duration)
|
||||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||||
require.Equal(t, false, req.Dimensions.ReportClick)
|
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(0), req.Dimensions.ClickedLink)
|
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigurationSuccess_fed(t *testing.T) {
|
func TestConfigurationSuccess_fed(t *testing.T) {
|
||||||
@ -71,7 +71,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
|||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
require.Equal(t, 10, req.Values.Duration)
|
require.Equal(t, 10, req.Values.Duration)
|
||||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||||
require.Equal(t, true, req.Dimensions.ReportClick)
|
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||||
require.Equal(t, false, req.Dimensions.ReportSent)
|
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||||
require.Equal(t, uint64(42), req.Dimensions.ClickedLink)
|
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,10 +124,12 @@ func (user *User) SendConfigStatusProgress() {
|
|||||||
if !user.configStatus.IsPending() {
|
if !user.configStatus.IsPending() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder configstatus.ConfigProgressBuilder
|
var builder configstatus.ConfigProgressBuilder
|
||||||
progress := builder.New(user.configStatus.Data)
|
progress := builder.New(user.configStatus.Data)
|
||||||
if progress.Values.NbDaySinceLast == 0 || progress.Values.NbDay == 0 {
|
if progress.Values.NbDay == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if progress.Values.NbDaySinceLast == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -514,9 +514,9 @@ 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, but we should
|
// 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.
|
// 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 {
|
if (event.Message.IsDraft() || (event.Message.Flags&proton.MessageFlagSent != 0)) && event.Action == proton.EventUpdate {
|
||||||
updates, err := user.handleUpdateDraftEvent(
|
updates, err := user.handleUpdateDraftOrSentMessage(
|
||||||
logging.WithLogrusField(ctx, "action", "update draft"),
|
logging.WithLogrusField(ctx, "action", "update draft or sent message"),
|
||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -701,18 +701,19 @@ func (user *User) handleDeleteMessageEvent(_ context.Context, event proton.Messa
|
|||||||
}, user.updateChLock)
|
}, 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) {
|
return safe.RLockRetErr(func() ([]imap.Update, error) {
|
||||||
user.log.WithFields(logrus.Fields{
|
user.log.WithFields(logrus.Fields{
|
||||||
"messageID": event.ID,
|
"messageID": event.ID,
|
||||||
"subject": logging.Sensitive(event.Message.Subject),
|
"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())
|
full, err := user.client.GetFullMessage(ctx, event.Message.ID, newProtonAPIScheduler(user.panicHandler), proton.NewDefaultAttachmentAllocator())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the message is not found, it means that it has been deleted before we could fetch it.
|
// 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 {
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user