From 531368da8606b47e62f29717095007ea28159f79 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Wed, 30 Oct 2024 12:36:21 +0100 Subject: [PATCH 01/19] feat(BRIDGE-252): restored the -h shortcut shortcut for the CLI --help switch. --- internal/app/app.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/app/app.go b/internal/app/app.go index be0ce6fe..b26d6cc2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -205,6 +205,7 @@ func New() *cli.App { // We override the default help value because we want "Show" to be capitalized cli.HelpFlag = &cli.BoolFlag{ Name: "help", + Aliases: []string{"h"}, Usage: "Show help", DisableDefaultText: true, } From 6647231278824fe889390a56546f4ab989e598cf Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Wed, 30 Oct 2024 11:45:50 +0100 Subject: [PATCH 02/19] chore: (BRIDGE-253) removing unused telemetry (activation and troubleshooting) --- internal/bridge/bug_report.go | 7 - internal/bridge/config_status.go | 46 ---- internal/bridge/settings.go | 3 +- internal/bridge/types.go | 1 - internal/bridge/user.go | 19 +- internal/bridge/user_events.go | 3 +- internal/configstatus/config_status.go | 228 ---------------- internal/configstatus/config_status_test.go | 252 ------------------ internal/configstatus/configuration_abort.go | 59 ---- .../configstatus/configuration_abort_test.go | 75 ------ .../configstatus/configuration_progress.go | 60 ----- .../configuration_progress_test.go | 100 ------- .../configstatus/configuration_recovery.go | 63 ----- .../configuration_recovery_test.go | 79 ------ .../configstatus/configuration_success.go | 61 ----- .../configuration_success_test.go | 77 ------ internal/configstatus/types_config_status.go | 56 ---- .../bridge-gui-tester/GRPCService.cpp | 26 -- .../bridge-gui-tester/GRPCService.h | 3 - .../bridge-gui/bridge-gui/QMLBackend.cpp | 28 -- .../bridge-gui/bridge-gui/QMLBackend.h | 3 - .../bridge-gui/bridge-gui/qml/HelpView.qml | 1 - .../bridgepp/bridgepp/GRPC/GRPCClient.cpp | 26 -- .../bridgepp/bridgepp/GRPC/GRPCClient.h | 5 - internal/frontend/grpc/bridge.pb.go | 216 +++++++-------- internal/frontend/grpc/bridge.proto | 5 - internal/frontend/grpc/bridge_grpc.pb.go | 113 -------- internal/frontend/grpc/service_telemetry.go | 44 --- internal/locations/locations.go | 14 - internal/services/imapservice/connector.go | 8 +- internal/services/imapservice/service.go | 11 - .../imapservice/service_address_events.go | 1 - internal/services/smtp/accounts.go | 5 - internal/services/smtp/service.go | 9 - internal/services/useridentity/mocks/mocks.go | 35 --- internal/services/useridentity/service.go | 3 - .../services/useridentity/service_test.go | 3 +- internal/services/useridentity/telemetry.go | 22 -- internal/user/config_status.go | 219 --------------- internal/user/user.go | 38 +-- internal/user/user_test.go | 1 - tests/config_status_test.go | 187 ------------- tests/features/bridge/config_status.feature | 79 ------ tests/steps_test.go | 7 - 44 files changed, 107 insertions(+), 2194 deletions(-) delete mode 100644 internal/bridge/config_status.go delete mode 100644 internal/configstatus/config_status.go delete mode 100644 internal/configstatus/config_status_test.go delete mode 100644 internal/configstatus/configuration_abort.go delete mode 100644 internal/configstatus/configuration_abort_test.go delete mode 100644 internal/configstatus/configuration_progress.go delete mode 100644 internal/configstatus/configuration_progress_test.go delete mode 100644 internal/configstatus/configuration_recovery.go delete mode 100644 internal/configstatus/configuration_recovery_test.go delete mode 100644 internal/configstatus/configuration_success.go delete mode 100644 internal/configstatus/configuration_success_test.go delete mode 100644 internal/configstatus/types_config_status.go delete mode 100644 internal/frontend/grpc/service_telemetry.go delete mode 100644 internal/services/useridentity/telemetry.go delete mode 100644 internal/user/config_status.go delete mode 100644 tests/config_status_test.go delete mode 100644 tests/features/bridge/config_status.feature diff --git a/internal/bridge/bug_report.go b/internal/bridge/bug_report.go index e43c89be..a33219bd 100644 --- a/internal/bridge/bug_report.go +++ b/internal/bridge/bug_report.go @@ -25,7 +25,6 @@ import ( "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/logging" - "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/vault" ) @@ -80,12 +79,6 @@ func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error return err } - safe.RLock(func() { - for _, user := range bridge.users { - user.ReportBugSent() - } - }, bridge.usersLock) - // if we have a token we can append more attachment to the bugReport for i, att := range attachments { if i == 0 && report.IncludeLogs { diff --git a/internal/bridge/config_status.go b/internal/bridge/config_status.go deleted file mode 100644 index 5b17bc85..00000000 --- a/internal/bridge/config_status.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2024 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 . - -package bridge - -import ( - "github.com/ProtonMail/proton-bridge/v3/internal/safe" -) - -func (bridge *Bridge) ReportBugClicked() { - safe.RLock(func() { - for _, user := range bridge.users { - user.ReportBugClicked() - } - }, bridge.usersLock) -} - -func (bridge *Bridge) AutoconfigUsed(client string) { - safe.RLock(func() { - for _, user := range bridge.users { - user.AutoconfigUsed(client) - } - }, bridge.usersLock) -} - -func (bridge *Bridge) ExternalLinkClicked(article string) { - safe.RLock(func() { - for _, user := range bridge.users { - user.ExternalLinkClicked(article) - } - }, bridge.usersLock) -} diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index 1cfee6e0..7478fabf 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -318,11 +318,10 @@ func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleL // Note: it does not clear the keychain. The only entry in the keychain is the vault password, // which we need at next startup to decrypt the vault. func (bridge *Bridge) FactoryReset(ctx context.Context) { - useTelemetry := !bridge.GetTelemetryDisabled() // Delete all the users. safe.Lock(func() { for _, user := range bridge.users { - bridge.logoutUser(ctx, user, true, true, useTelemetry) + bridge.logoutUser(ctx, user, true, true) } }, bridge.usersLock) diff --git a/internal/bridge/types.go b/internal/bridge/types.go index e3516809..e9b6978d 100644 --- a/internal/bridge/types.go +++ b/internal/bridge/types.go @@ -28,7 +28,6 @@ type Locator interface { ProvideLogsPath() (string, error) ProvideGluonCachePath() (string, error) ProvideGluonDataPath() (string, error) - ProvideStatsPath() (string, error) GetLicenseFilePath() string GetDependencyLicensesLink() string Clear(...string) error diff --git a/internal/bridge/user.go b/internal/bridge/user.go index fe87ae20..efde55f4 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -255,7 +255,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error { return ErrNoSuchUser } - bridge.logoutUser(ctx, user, true, false, false) + bridge.logoutUser(ctx, user, true, false) bridge.publish(events.UserLoggedOut{ UserID: userID, @@ -280,7 +280,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error { } if user, ok := bridge.users[userID]; ok { - bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled()) + bridge.logoutUser(ctx, user, true, true) } if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil { @@ -358,7 +358,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, return user.BadEventFeedbackResync(ctx) } - bridge.logoutUser(ctx, user, true, false, false) + bridge.logoutUser(ctx, user, true, false) bridge.publish(events.UserLoggedOut{ UserID: userID, @@ -527,11 +527,6 @@ func (bridge *Bridge) addUserWithVault( vault *vault.User, isNew bool, ) error { - statsPath, err := bridge.locator.ProvideStatsPath() - if err != nil { - return fmt.Errorf("failed to get Statistics directory: %w", err) - } - syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath() if err != nil { return fmt.Errorf("failed to get IMAP sync config path: %w", err) @@ -546,7 +541,6 @@ func (bridge *Bridge) addUserWithVault( bridge.panicHandler, bridge.vault.GetShowAllMail(), bridge.vault.GetMaxSyncMemory(), - statsPath, bridge, bridge.serverManager, bridge.serverManager, @@ -611,14 +605,9 @@ func (bridge *Bridge) newVaultUser( } // logout logs out the given user, optionally logging them out from the API too. -func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) { +func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) { defer delete(bridge.users, user.ID()) - // if this is actually a remove account - if withData && withAPI { - user.SendConfigStatusAbort(ctx, withTelemetry) - } - logUser.WithFields(logrus.Fields{ "userID": user.ID(), "withAPI": withAPI, diff --git a/internal/bridge/user_events.go b/internal/bridge/user_events.go index c41f771c..f9069ce2 100644 --- a/internal/bridge/user_events.go +++ b/internal/bridge/user_events.go @@ -43,8 +43,7 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) { safe.Lock(func() { - bridge.logoutUser(ctx, user, false, false, false) - user.ReportConfigStatusFailure("User deauth.") + bridge.logoutUser(ctx, user, false, false) }, bridge.usersLock) } diff --git a/internal/configstatus/config_status.go b/internal/configstatus/config_status.go deleted file mode 100644 index a8ec2e71..00000000 --- a/internal/configstatus/config_status.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import ( - "encoding/json" - "fmt" - "os" - "strconv" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/safe" - "github.com/sirupsen/logrus" -) - -const version = "1.0.0" - -func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) { - status := ConfigurationStatus{ - FilePath: filepath, - DataLock: safe.NewRWMutex(), - Data: &ConfigurationStatusData{}, - } - - if _, err := os.Stat(filepath); err == nil { - if err := status.Load(); err == nil { - return &status, nil - } - logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.") - } - - status.Data.init() - if err := status.Save(); err != nil { - return &status, err - } - return &status, nil -} - -func (status *ConfigurationStatus) Load() error { - bytes, err := os.ReadFile(status.FilePath) - if err != nil { - return err - } - - var metadata MetadataOnly - if err := json.Unmarshal(bytes, &metadata); err != nil { - return err - } - - if metadata.Metadata.Version != version { - return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version) - } - - return json.Unmarshal(bytes, status.Data) -} - -func (status *ConfigurationStatus) Save() error { - temp := status.FilePath + "_temp" - f, err := os.Create(temp) //nolint:gosec - if err != nil { - return err - } - enc := json.NewEncoder(f) - enc.SetIndent("", " ") - err = enc.Encode(status.Data) - if err := f.Close(); err != nil { - logrus.WithError(err).Error("Error while closing configstatus file.") - } - if err != nil { - return err - } - - return os.Rename(temp, status.FilePath) -} - -func (status *ConfigurationStatus) IsPending() bool { - status.DataLock.RLock() - defer status.DataLock.RUnlock() - - return !status.Data.DataV1.PendingSince.IsZero() -} - -func (status *ConfigurationStatus) isPendingSinceMin() int { - if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 { //nolint:predeclared - return min - } - return 0 -} - -func (status *ConfigurationStatus) IsFromFailure() bool { - status.DataLock.RLock() - defer status.DataLock.RUnlock() - - return status.Data.DataV1.FailureDetails != "" -} - -func (status *ConfigurationStatus) ApplySuccess() error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - status.Data.init() - status.Data.DataV1.PendingSince = time.Time{} - return status.Save() -} - -func (status *ConfigurationStatus) ApplyFailure(err string) error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - status.Data.init() - status.Data.DataV1.FailureDetails = err - return status.Save() -} - -func (status *ConfigurationStatus) ApplyProgress() error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - status.Data.DataV1.LastProgress = time.Now() - return status.Save() -} - -func (status *ConfigurationStatus) RecordLinkClicked(link uint64) error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - if !status.Data.hasLinkClicked(link) { - status.Data.setClickedLink(link) - return status.Save() - } - return nil -} - -func (status *ConfigurationStatus) ReportClicked() error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - if !status.Data.DataV1.ReportClick { - status.Data.DataV1.ReportClick = true - return status.Save() - } - return nil -} - -func (status *ConfigurationStatus) ReportSent() error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - if !status.Data.DataV1.ReportSent { - status.Data.DataV1.ReportSent = true - return status.Save() - } - return nil -} - -func (status *ConfigurationStatus) AutoconfigUsed(client string) error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - - if client != status.Data.DataV1.Autoconf { - status.Data.DataV1.Autoconf = client - return status.Save() - } - return nil -} - -func (status *ConfigurationStatus) Remove() error { - status.DataLock.Lock() - defer status.DataLock.Unlock() - return os.Remove(status.FilePath) -} - -func (data *ConfigurationStatusData) init() { - data.Metadata = Metadata{ - Version: version, - } - data.DataV1.PendingSince = time.Now() - data.DataV1.LastProgress = time.Time{} - data.DataV1.Autoconf = "" - data.DataV1.ClickedLink = 0 - data.DataV1.ReportSent = false - data.DataV1.ReportClick = false - data.DataV1.FailureDetails = "" -} - -func (data *ConfigurationStatusData) setClickedLink(pos uint64) { - data.DataV1.ClickedLink |= 1 << pos -} - -func (data *ConfigurationStatusData) hasLinkClicked(pos uint64) bool { - val := data.DataV1.ClickedLink & (1 << pos) - return val > 0 -} - -func (data *ConfigurationStatusData) clickedLinkToString() string { - var str = "" - var first = true - for i := 0; i < 64; i++ { - if data.hasLinkClicked(uint64(i)) { //nolint:gosec // disable G115 - if !first { - str += "," - } else { - first = false - str += "[" - } - str += strconv.Itoa(i) - } - } - if str != "" { - str += "]" - } - return str -} diff --git a/internal/configstatus/config_status_test.go b/internal/configstatus/config_status_test.go deleted file mode 100644 index 746b22f3..00000000 --- a/internal/configstatus/config_status_test.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus_test - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/stretchr/testify/require" -) - -func TestConfigStatus_init_virgin(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - require.Equal(t, "1.0.0", config.Data.Metadata.Version) - - require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero()) - - require.Equal(t, "", config.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink) - require.Equal(t, false, config.Data.DataV1.ReportSent) - require.Equal(t, false, config.Data.DataV1.ReportClick) - require.Equal(t, "", config.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_init_existing(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{Autoconf: "Mr TBird"}, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config.Data.Metadata.Version) - require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf) -} - -func TestConfigStatus_init_bad_version(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "2.0.0"}, - DataV1: configstatus.DataV1{Autoconf: "Mr TBird"}, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config.Data.Metadata.Version) - require.Equal(t, "", config.Data.DataV1.Autoconf) -} - -func TestConfigStatus_IsPending(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, true, config.IsPending()) - config.Data.DataV1.PendingSince = time.Time{} - require.Equal(t, false, config.IsPending()) -} - -func TestConfigStatus_IsFromFailure(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, false, config.IsFromFailure()) - config.Data.DataV1.FailureDetails = "test" - require.Equal(t, true, config.IsFromFailure()) -} - -func TestConfigStatus_ApplySuccess(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, true, config.IsPending()) - require.NoError(t, config.ApplySuccess()) - require.Equal(t, false, config.IsPending()) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink) - require.Equal(t, false, config2.Data.DataV1.ReportSent) - require.Equal(t, false, config2.Data.DataV1.ReportClick) - require.Equal(t, "", config2.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_ApplyFailure(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - require.NoError(t, config.ApplySuccess()) - - require.NoError(t, config.ApplyFailure("Big Failure")) - require.Equal(t, true, config.IsFromFailure()) - require.Equal(t, true, config.IsPending()) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink) - require.Equal(t, false, config2.Data.DataV1.ReportSent) - require.Equal(t, false, config2.Data.DataV1.ReportClick) - require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_ApplyProgress(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, true, config.IsPending()) - require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero()) - - require.NoError(t, config.ApplyProgress()) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink) - require.Equal(t, false, config2.Data.DataV1.ReportSent) - require.Equal(t, false, config2.Data.DataV1.ReportClick) - require.Equal(t, "", config2.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_RecordLinkClicked(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink) - require.NoError(t, config.RecordLinkClicked(0)) - require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink) - require.NoError(t, config.RecordLinkClicked(1)) - require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink) - require.Equal(t, false, config2.Data.DataV1.ReportSent) - require.Equal(t, false, config2.Data.DataV1.ReportClick) - require.Equal(t, "", config2.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_ReportClicked(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, false, config.Data.DataV1.ReportClick) - require.NoError(t, config.ReportClicked()) - require.Equal(t, true, config.Data.DataV1.ReportClick) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink) - require.Equal(t, false, config2.Data.DataV1.ReportSent) - require.Equal(t, true, config2.Data.DataV1.ReportClick) - require.Equal(t, "", config2.Data.DataV1.FailureDetails) -} - -func TestConfigStatus_ReportSent(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, false, config.Data.DataV1.ReportSent) - require.NoError(t, config.ReportSent()) - require.Equal(t, true, config.Data.DataV1.ReportSent) - - config2, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - require.Equal(t, "1.0.0", config2.Data.Metadata.Version) - require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero()) - require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero()) - require.Equal(t, "", config2.Data.DataV1.Autoconf) - require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink) - require.Equal(t, true, config2.Data.DataV1.ReportSent) - require.Equal(t, false, config2.Data.DataV1.ReportClick) - require.Equal(t, "", config2.Data.DataV1.FailureDetails) -} - -func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error { - f, err := os.Create(file) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - - return json.NewEncoder(f).Encode(data) -} diff --git a/internal/configstatus/configuration_abort.go b/internal/configstatus/configuration_abort.go deleted file mode 100644 index d3cba23c..00000000 --- a/internal/configstatus/configuration_abort.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import ( - "strconv" -) - -type ConfigAbortValues struct { - Duration int `json:"duration"` -} - -type ConfigAbortDimensions struct { - ReportClick string `json:"report_click"` - ReportSent string `json:"report_sent"` - ClickedLink string `json:"clicked_link"` -} - -type ConfigAbortData struct { - MeasurementGroup string - Event string - Values ConfigSuccessValues - Dimensions ConfigSuccessDimensions -} - -type ConfigAbortBuilder struct{} - -func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData { - config.DataLock.RLock() - defer config.DataLock.RUnlock() - - return ConfigAbortData{ - MeasurementGroup: "bridge.any.configuration", - Event: "bridge_config_abort", - Values: ConfigSuccessValues{ - Duration: config.isPendingSinceMin(), - }, - Dimensions: ConfigSuccessDimensions{ - ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick), - ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent), - ClickedLink: config.Data.clickedLinkToString(), - }, - } -} diff --git a/internal/configstatus/configuration_abort_test.go b/internal/configstatus/configuration_abort_test.go deleted file mode 100644 index 688f68a1..00000000 --- a/internal/configstatus/configuration_abort_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus_test - -import ( - "path/filepath" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/stretchr/testify/require" -) - -func TestConfigurationAbort_default(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigAbortBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_abort", req.Event) - require.Equal(t, 0, req.Values.Duration) - require.Equal(t, "false", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "", req.Dimensions.ClickedLink) -} - -func TestConfigurationAbort_fed(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{ - PendingSince: time.Now().Add(-10 * time.Minute), - LastProgress: time.Time{}, - Autoconf: "Mr TBird", - ClickedLink: 42, - ReportSent: false, - ReportClick: true, - FailureDetails: "Not an error", - }, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigAbortBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_abort", req.Event) - require.Equal(t, 10, req.Values.Duration) - require.Equal(t, "true", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink) -} diff --git a/internal/configstatus/configuration_progress.go b/internal/configstatus/configuration_progress.go deleted file mode 100644 index e8d15bf0..00000000 --- a/internal/configstatus/configuration_progress.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import "time" - -type ConfigProgressValues struct { - NbDay int `json:"nb_day"` - NbDaySinceLast int `json:"nb_day_since_last"` -} - -type ConfigProgressData struct { - MeasurementGroup string - Event string - Values ConfigProgressValues - Dimensions struct{} -} - -type ConfigProgressBuilder struct{} - -func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData { - config.DataLock.RLock() - defer config.DataLock.RUnlock() - - return ConfigProgressData{ - MeasurementGroup: "bridge.any.configuration", - Event: "bridge_config_progress", - Values: ConfigProgressValues{ - NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince), - NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress), - }, - } -} - -func numberOfDay(now, prev time.Time) int { - if now.IsZero() || prev.IsZero() { - return 1 - } - if now.Year() > prev.Year() { - return (365 * (now.Year() - prev.Year())) + now.YearDay() - prev.YearDay() - } else if now.YearDay() > prev.YearDay() { - return now.YearDay() - prev.YearDay() - } - return 0 -} diff --git a/internal/configstatus/configuration_progress_test.go b/internal/configstatus/configuration_progress_test.go deleted file mode 100644 index 8d9b5f1e..00000000 --- a/internal/configstatus/configuration_progress_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus_test - -import ( - "path/filepath" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/stretchr/testify/require" -) - -func TestConfigurationProgress_default(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigProgressBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_progress", req.Event) - require.Equal(t, 0, req.Values.NbDay) - require.Equal(t, 1, req.Values.NbDaySinceLast) -} - -func TestConfigurationProgress_fed(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{ - PendingSince: time.Now().AddDate(0, 0, -5), - LastProgress: time.Now().AddDate(0, 0, -2), - Autoconf: "Mr TBird", - ClickedLink: 42, - ReportSent: false, - ReportClick: true, - FailureDetails: "Not an error", - }, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigProgressBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_progress", req.Event) - require.Equal(t, 5, req.Values.NbDay) - require.Equal(t, 2, req.Values.NbDaySinceLast) -} - -func TestConfigurationProgress_fed_year_change(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{ - PendingSince: time.Now().AddDate(-1, 0, -5), - LastProgress: time.Now().AddDate(0, 0, -2), - Autoconf: "Mr TBird", - ClickedLink: 42, - ReportSent: false, - ReportClick: true, - FailureDetails: "Not an error", - }, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigProgressBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_progress", req.Event) - require.True(t, (req.Values.NbDay == 370) || (req.Values.NbDay == 371)) // leap year is accounted for in the simplest manner. - require.Equal(t, 2, req.Values.NbDaySinceLast) -} diff --git a/internal/configstatus/configuration_recovery.go b/internal/configstatus/configuration_recovery.go deleted file mode 100644 index f0907821..00000000 --- a/internal/configstatus/configuration_recovery.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import ( - "strconv" -) - -type ConfigRecoveryValues struct { - Duration int `json:"duration"` -} - -type ConfigRecoveryDimensions struct { - Autoconf string `json:"autoconf"` - ReportClick string `json:"report_click"` - ReportSent string `json:"report_sent"` - ClickedLink string `json:"clicked_link"` - FailureDetails string `json:"failure_details"` -} - -type ConfigRecoveryData struct { - MeasurementGroup string - Event string - Values ConfigRecoveryValues - Dimensions ConfigRecoveryDimensions -} - -type ConfigRecoveryBuilder struct{} - -func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData { - config.DataLock.RLock() - defer config.DataLock.RUnlock() - - return ConfigRecoveryData{ - MeasurementGroup: "bridge.any.configuration", - Event: "bridge_config_recovery", - Values: ConfigRecoveryValues{ - Duration: config.isPendingSinceMin(), - }, - Dimensions: ConfigRecoveryDimensions{ - Autoconf: config.Data.DataV1.Autoconf, - ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick), - ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent), - ClickedLink: config.Data.clickedLinkToString(), - FailureDetails: config.Data.DataV1.FailureDetails, - }, - } -} diff --git a/internal/configstatus/configuration_recovery_test.go b/internal/configstatus/configuration_recovery_test.go deleted file mode 100644 index 16266667..00000000 --- a/internal/configstatus/configuration_recovery_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus_test - -import ( - "path/filepath" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/stretchr/testify/require" -) - -func TestConfigurationRecovery_default(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigRecoveryBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_recovery", req.Event) - require.Equal(t, 0, req.Values.Duration) - require.Equal(t, "", req.Dimensions.Autoconf) - require.Equal(t, "false", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "", req.Dimensions.ClickedLink) - require.Equal(t, "", req.Dimensions.FailureDetails) -} - -func TestConfigurationRecovery_fed(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{ - PendingSince: time.Now().Add(-10 * time.Minute), - LastProgress: time.Time{}, - Autoconf: "Mr TBird", - ClickedLink: 42, - ReportSent: false, - ReportClick: true, - FailureDetails: "Not an error", - }, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigRecoveryBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_recovery", req.Event) - require.Equal(t, 10, req.Values.Duration) - require.Equal(t, "Mr TBird", req.Dimensions.Autoconf) - require.Equal(t, "true", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink) - require.Equal(t, "Not an error", req.Dimensions.FailureDetails) -} diff --git a/internal/configstatus/configuration_success.go b/internal/configstatus/configuration_success.go deleted file mode 100644 index 86b3de34..00000000 --- a/internal/configstatus/configuration_success.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import ( - "strconv" -) - -type ConfigSuccessValues struct { - Duration int `json:"duration"` -} - -type ConfigSuccessDimensions struct { - Autoconf string `json:"autoconf"` - ReportClick string `json:"report_click"` - ReportSent string `json:"report_sent"` - ClickedLink string `json:"clicked_link"` -} - -type ConfigSuccessData struct { - MeasurementGroup string - Event string - Values ConfigSuccessValues - Dimensions ConfigSuccessDimensions -} - -type ConfigSuccessBuilder struct{} - -func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData { - config.DataLock.RLock() - defer config.DataLock.RUnlock() - - return ConfigSuccessData{ - MeasurementGroup: "bridge.any.configuration", - Event: "bridge_config_success", - Values: ConfigSuccessValues{ - Duration: config.isPendingSinceMin(), - }, - Dimensions: ConfigSuccessDimensions{ - Autoconf: config.Data.DataV1.Autoconf, - ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick), - ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent), - ClickedLink: config.Data.clickedLinkToString(), - }, - } -} diff --git a/internal/configstatus/configuration_success_test.go b/internal/configstatus/configuration_success_test.go deleted file mode 100644 index 332d8f45..00000000 --- a/internal/configstatus/configuration_success_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus_test - -import ( - "path/filepath" - "testing" - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/stretchr/testify/require" -) - -func TestConfigurationSuccess_default(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigSuccessBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_success", req.Event) - require.Equal(t, 0, req.Values.Duration) - require.Equal(t, "", req.Dimensions.Autoconf) - require.Equal(t, "false", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "", req.Dimensions.ClickedLink) -} - -func TestConfigurationSuccess_fed(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "dummy.json") - var data = configstatus.ConfigurationStatusData{ - Metadata: configstatus.Metadata{Version: "1.0.0"}, - DataV1: configstatus.DataV1{ - PendingSince: time.Now().Add(-10 * time.Minute), - LastProgress: time.Time{}, - Autoconf: "Mr TBird", - ClickedLink: 42, - ReportSent: false, - ReportClick: true, - FailureDetails: "Not an error", - }, - } - require.NoError(t, dumpConfigStatusInFile(&data, file)) - - config, err := configstatus.LoadConfigurationStatus(file) - require.NoError(t, err) - - var builder = configstatus.ConfigSuccessBuilder{} - req := builder.New(config) - - require.Equal(t, "bridge.any.configuration", req.MeasurementGroup) - require.Equal(t, "bridge_config_success", req.Event) - require.Equal(t, 10, req.Values.Duration) - require.Equal(t, "Mr TBird", req.Dimensions.Autoconf) - require.Equal(t, "true", req.Dimensions.ReportClick) - require.Equal(t, "false", req.Dimensions.ReportSent) - require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink) -} diff --git a/internal/configstatus/types_config_status.go b/internal/configstatus/types_config_status.go deleted file mode 100644 index a5474574..00000000 --- a/internal/configstatus/types_config_status.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2024 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 . - -package configstatus - -import ( - "time" - - "github.com/ProtonMail/proton-bridge/v3/internal/safe" -) - -const ProgressCheckInterval = time.Hour - -type Metadata struct { - Version string `json:"version"` -} - -type MetadataOnly struct { - Metadata Metadata `json:"metadata"` -} - -type DataV1 struct { - PendingSince time.Time `json:"pending_since"` - LastProgress time.Time `json:"last_progress"` - Autoconf string `json:"auto_conf"` - ClickedLink uint64 `json:"clicked_link"` - ReportSent bool `json:"report_sent"` - ReportClick bool `json:"report_click"` - FailureDetails string `json:"failure_details"` -} - -type ConfigurationStatusData struct { - Metadata Metadata `json:"metadata"` - DataV1 DataV1 `json:"dataV1"` -} - -type ConfigurationStatus struct { - FilePath string - DataLock safe.RWMutex - - Data *ConfigurationStatusData -} diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp index 917129a1..9eb0fac2 100644 --- a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp +++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp @@ -846,32 +846,6 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty } -//**************************************************************************************************************************************************** -/// \param[in] request The request. -//**************************************************************************************************************************************************** -Status GRPCService::ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) { - app().log().debug(QString("%1 - URL = %2").arg(__FUNCTION__, QString::fromStdString(request->value()))); - return Status::OK; -} - -//**************************************************************************************************************************************************** -// -//**************************************************************************************************************************************************** -Status GRPCService::ReportBugClicked(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) { - app().log().debug(__FUNCTION__); - return Status::OK; -} - - -//**************************************************************************************************************************************************** -/// \param[in] request The request. -//**************************************************************************************************************************************************** -Status GRPCService::AutoconfigClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) { - app().log().debug(QString("%1 - Client = %2").arg(__FUNCTION__, QString::fromStdString(request->value()))); - return Status::OK; -} - - //**************************************************************************************************************************************************** /// \param[in] request The request /// \param[in] writer The writer diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h index 60490971..9222952e 100644 --- a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h +++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h @@ -97,9 +97,6 @@ public: // member functions. grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override; grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override; grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; - grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override; - grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; - grpc::Status ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override; grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override; grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override; bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream. diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp index 06a0d673..962fb664 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp @@ -303,7 +303,6 @@ void QMLBackend::openExternalLink(QString const &url) { HANDLE_EXCEPTION( QString const u = url.isEmpty() ? bridgeKBUrl : url; QDesktopServices::openUrl(u); - emit notifyExternalLinkClicked(u); ) } @@ -1095,33 +1094,6 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync) ) } -//**************************************************************************************************************************************************** -/// -//**************************************************************************************************************************************************** -void QMLBackend::notifyReportBugClicked() const { - HANDLE_EXCEPTION( - app().grpc().reportBugClicked(); - ) -} -//**************************************************************************************************************************************************** -/// \param[in] client The selected Mail client for autoconfig. -//**************************************************************************************************************************************************** -void QMLBackend::notifyAutoconfigClicked(QString const &client) const { - HANDLE_EXCEPTION( - app().grpc().autoconfigClicked(client); - ) -} - -//**************************************************************************************************************************************************** -/// \param[in] article The url of the KB article. -//**************************************************************************************************************************************************** -void QMLBackend::notifyExternalLinkClicked(QString const &article) const { - HANDLE_EXCEPTION( - app().grpc().externalLinkClicked(article); - ) -} - - //**************************************************************************************************************************************************** // //**************************************************************************************************************************************************** diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h index 55ad0e8f..2519d7f2 100644 --- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h +++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.h @@ -213,9 +213,6 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge void onVersionChanged(); ///< Slot for the version change signal. void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event. - void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event. - void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event. - void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event. void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'. void userNotificationDismissed(); ///< Slot to pop the notification from the stack and display the rest. diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/HelpView.qml b/internal/frontend/bridge-gui/bridge-gui/qml/HelpView.qml index ac2704f0..59e2159e 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/HelpView.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/HelpView.qml @@ -83,7 +83,6 @@ SettingsView { onClicked: { Backend.updateCurrentMailClient(); - Backend.notifyReportBugClicked(); root.parent.showBugReport(); } } diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp index 48aafca3..304bba78 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.cpp @@ -1572,32 +1572,6 @@ UPClientContext GRPCClient::clientContext() const { return ctx; } -//**************************************************************************************************************************************************** -/// \return the status for the gRPC call. -//**************************************************************************************************************************************************** -grpc::Status GRPCClient::reportBugClicked() { - return this->logGRPCCallStatus(stub_->ReportBugClicked(this->clientContext().get(), empty, &empty), __FUNCTION__); -} - -//**************************************************************************************************************************************************** -/// \param[in] client The client string. -/// \return the status for the gRPC call. -//**************************************************************************************************************************************************** -grpc::Status GRPCClient::autoconfigClicked(QString const &client) { - StringValue s; - s.set_value(client.toStdString()); - return this->logGRPCCallStatus(stub_->AutoconfigClicked(this->clientContext().get(), s, &empty), __FUNCTION__); -} - -//**************************************************************************************************************************************************** -/// \param[in] link The clicked link. -/// \return the status for the gRPC call. -//**************************************************************************************************************************************************** -grpc::Status GRPCClient::externalLinkClicked(QString const &link) { - StringValue s; - s.set_value(link.toStdString()); - return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__); -} //**************************************************************************************************************************************************** // diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h index b5120792..992b2ea9 100644 --- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h +++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCClient.h @@ -232,11 +232,6 @@ signals: void syncFinished(QString const &userID); void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs); -public: // telemetry related calls - grpc::Status reportBugClicked(); ///< Performs the 'reportBugClicked' call. - grpc::Status autoconfigClicked(QString const &userID); ///< Performs the 'AutoconfigClicked' call. - grpc::Status externalLinkClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call. - public: // keychain related calls grpc::Status availableKeychains(QStringList &outKeychains); grpc::Status currentKeychain(QString &outKeychain); diff --git a/internal/frontend/grpc/bridge.pb.go b/internal/frontend/grpc/bridge.pb.go index 87fd8764..c1cb6498 100644 --- a/internal/frontend/grpc/bridge.pb.go +++ b/internal/frontend/grpc/bridge.pb.go @@ -5425,7 +5425,7 @@ var file_bridge_proto_rawDesc = []byte{ 0x4c, 0x53, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, - 0x32, 0x99, 0x23, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, + 0x32, 0xbd, 0x21, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, @@ -5666,51 +5666,37 @@ var file_bridge_proto_rawDesc = []byte{ 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x42, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x43, - 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, - 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x4b, 0x0a, 0x13, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4c, 0x69, 0x6e, - 0x6b, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, - 0x0a, 0x19, 0x49, 0x73, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x47, 0x0a, 0x15, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, - 0x72, 0x74, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, + 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x19, 0x49, 0x73, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d, 0x54, - 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x70, 0x61, 0x69, 0x72, 0x12, 0x16, 0x2e, 0x67, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x47, 0x0a, 0x15, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x54, 0x4c, + 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, - 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x15, + 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, + 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, + 0x53, 0x74, 0x6f, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x3f, 0x0a, 0x0d, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x70, 0x61, 0x69, 0x72, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, + 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -5938,81 +5924,75 @@ var file_bridge_proto_depIdxs = []int32{ 80, // 121: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue 80, // 122: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue 18, // 123: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest - 81, // 124: grpc.Bridge.ReportBugClicked:input_type -> google.protobuf.Empty - 80, // 125: grpc.Bridge.AutoconfigClicked:input_type -> google.protobuf.StringValue - 80, // 126: grpc.Bridge.ExternalLinkClicked:input_type -> google.protobuf.StringValue - 81, // 127: grpc.Bridge.IsTLSCertificateInstalled:input_type -> google.protobuf.Empty - 81, // 128: grpc.Bridge.InstallTLSCertificate:input_type -> google.protobuf.Empty - 80, // 129: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue - 19, // 130: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest - 81, // 131: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty - 81, // 132: grpc.Bridge.TriggerRepair:input_type -> google.protobuf.Empty - 80, // 133: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue - 81, // 134: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty - 8, // 135: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse - 81, // 136: grpc.Bridge.Quit:output_type -> google.protobuf.Empty - 81, // 137: grpc.Bridge.Restart:output_type -> google.protobuf.Empty - 82, // 138: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue - 81, // 139: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty - 82, // 140: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue - 81, // 141: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty - 82, // 142: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue - 81, // 143: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty - 82, // 144: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue - 81, // 145: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty - 82, // 146: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue - 80, // 147: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue - 81, // 148: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty - 80, // 149: grpc.Bridge.Version:output_type -> google.protobuf.StringValue - 80, // 150: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue - 80, // 151: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue - 80, // 152: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue - 80, // 153: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue - 80, // 154: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue - 81, // 155: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty - 80, // 156: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue - 80, // 157: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue - 81, // 158: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty - 81, // 159: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty - 81, // 160: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty - 81, // 161: grpc.Bridge.RequestKnowledgeBaseSuggestions:output_type -> google.protobuf.Empty - 81, // 162: grpc.Bridge.Login:output_type -> google.protobuf.Empty - 81, // 163: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty - 81, // 164: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty - 81, // 165: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty - 81, // 166: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty - 81, // 167: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty - 81, // 168: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty - 82, // 169: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue - 80, // 170: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue - 81, // 171: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty - 81, // 172: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty - 82, // 173: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue - 12, // 174: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings - 81, // 175: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty - 80, // 176: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue - 82, // 177: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue - 13, // 178: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse - 81, // 179: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty - 80, // 180: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue - 17, // 181: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse - 14, // 182: grpc.Bridge.GetUser:output_type -> grpc.User - 81, // 183: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty - 81, // 184: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty - 81, // 185: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty - 81, // 186: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty - 81, // 187: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty - 81, // 188: grpc.Bridge.ReportBugClicked:output_type -> google.protobuf.Empty - 81, // 189: grpc.Bridge.AutoconfigClicked:output_type -> google.protobuf.Empty - 81, // 190: grpc.Bridge.ExternalLinkClicked:output_type -> google.protobuf.Empty - 82, // 191: grpc.Bridge.IsTLSCertificateInstalled:output_type -> google.protobuf.BoolValue - 81, // 192: grpc.Bridge.InstallTLSCertificate:output_type -> google.protobuf.Empty - 81, // 193: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty - 20, // 194: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent - 81, // 195: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty - 81, // 196: grpc.Bridge.TriggerRepair:output_type -> google.protobuf.Empty - 133, // [133:197] is the sub-list for method output_type - 69, // [69:133] is the sub-list for method input_type + 81, // 124: grpc.Bridge.IsTLSCertificateInstalled:input_type -> google.protobuf.Empty + 81, // 125: grpc.Bridge.InstallTLSCertificate:input_type -> google.protobuf.Empty + 80, // 126: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue + 19, // 127: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest + 81, // 128: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty + 81, // 129: grpc.Bridge.TriggerRepair:input_type -> google.protobuf.Empty + 80, // 130: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue + 81, // 131: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty + 8, // 132: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse + 81, // 133: grpc.Bridge.Quit:output_type -> google.protobuf.Empty + 81, // 134: grpc.Bridge.Restart:output_type -> google.protobuf.Empty + 82, // 135: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue + 81, // 136: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty + 82, // 137: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue + 81, // 138: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty + 82, // 139: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue + 81, // 140: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty + 82, // 141: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue + 81, // 142: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty + 82, // 143: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue + 80, // 144: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue + 81, // 145: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty + 80, // 146: grpc.Bridge.Version:output_type -> google.protobuf.StringValue + 80, // 147: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue + 80, // 148: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue + 80, // 149: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue + 80, // 150: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue + 80, // 151: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue + 81, // 152: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty + 80, // 153: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue + 80, // 154: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue + 81, // 155: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty + 81, // 156: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty + 81, // 157: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty + 81, // 158: grpc.Bridge.RequestKnowledgeBaseSuggestions:output_type -> google.protobuf.Empty + 81, // 159: grpc.Bridge.Login:output_type -> google.protobuf.Empty + 81, // 160: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty + 81, // 161: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty + 81, // 162: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty + 81, // 163: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty + 81, // 164: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty + 81, // 165: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty + 82, // 166: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue + 80, // 167: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue + 81, // 168: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty + 81, // 169: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty + 82, // 170: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue + 12, // 171: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings + 81, // 172: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty + 80, // 173: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue + 82, // 174: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue + 13, // 175: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse + 81, // 176: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty + 80, // 177: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue + 17, // 178: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse + 14, // 179: grpc.Bridge.GetUser:output_type -> grpc.User + 81, // 180: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty + 81, // 181: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty + 81, // 182: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty + 81, // 183: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty + 81, // 184: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty + 82, // 185: grpc.Bridge.IsTLSCertificateInstalled:output_type -> google.protobuf.BoolValue + 81, // 186: grpc.Bridge.InstallTLSCertificate:output_type -> google.protobuf.Empty + 81, // 187: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty + 20, // 188: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent + 81, // 189: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty + 81, // 190: grpc.Bridge.TriggerRepair:output_type -> google.protobuf.Empty + 130, // [130:191] is the sub-list for method output_type + 69, // [69:130] is the sub-list for method input_type 69, // [69:69] is the sub-list for extension type_name 69, // [69:69] is the sub-list for extension extendee 0, // [0:69] is the sub-list for field type_name diff --git a/internal/frontend/grpc/bridge.proto b/internal/frontend/grpc/bridge.proto index 369aebf5..2161487f 100644 --- a/internal/frontend/grpc/bridge.proto +++ b/internal/frontend/grpc/bridge.proto @@ -98,11 +98,6 @@ service Bridge { rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty); rpc ConfigureUserAppleMail(ConfigureAppleMailRequest) returns (google.protobuf.Empty); - // Telemetry - rpc ReportBugClicked(google.protobuf.Empty) returns (google.protobuf.Empty); - rpc AutoconfigClicked(google.protobuf.StringValue) returns (google.protobuf.Empty); - rpc ExternalLinkClicked(google.protobuf.StringValue) returns (google.protobuf.Empty); - // TLS certificate related calls rpc IsTLSCertificateInstalled(google.protobuf.Empty) returns (google.protobuf.BoolValue); rpc InstallTLSCertificate(google.protobuf.Empty) returns (google.protobuf.Empty); diff --git a/internal/frontend/grpc/bridge_grpc.pb.go b/internal/frontend/grpc/bridge_grpc.pb.go index 7b55d529..8573e9d5 100644 --- a/internal/frontend/grpc/bridge_grpc.pb.go +++ b/internal/frontend/grpc/bridge_grpc.pb.go @@ -93,9 +93,6 @@ const ( Bridge_LogoutUser_FullMethodName = "/grpc.Bridge/LogoutUser" Bridge_RemoveUser_FullMethodName = "/grpc.Bridge/RemoveUser" Bridge_ConfigureUserAppleMail_FullMethodName = "/grpc.Bridge/ConfigureUserAppleMail" - Bridge_ReportBugClicked_FullMethodName = "/grpc.Bridge/ReportBugClicked" - Bridge_AutoconfigClicked_FullMethodName = "/grpc.Bridge/AutoconfigClicked" - Bridge_ExternalLinkClicked_FullMethodName = "/grpc.Bridge/ExternalLinkClicked" Bridge_IsTLSCertificateInstalled_FullMethodName = "/grpc.Bridge/IsTLSCertificateInstalled" Bridge_InstallTLSCertificate_FullMethodName = "/grpc.Bridge/InstallTLSCertificate" Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates" @@ -170,10 +167,6 @@ type BridgeClient interface { LogoutUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) ConfigureUserAppleMail(ctx context.Context, in *ConfigureAppleMailRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - // Telemetry - ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) - AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) - ExternalLinkClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) // TLS certificate related calls IsTLSCertificateInstalled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) InstallTLSCertificate(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -688,33 +681,6 @@ func (c *bridgeClient) ConfigureUserAppleMail(ctx context.Context, in *Configure return out, nil } -func (c *bridgeClient) ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) { - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Bridge_ReportBugClicked_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *bridgeClient) AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) { - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Bridge_AutoconfigClicked_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *bridgeClient) ExternalLinkClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) { - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, Bridge_ExternalLinkClicked_FullMethodName, in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *bridgeClient) IsTLSCertificateInstalled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) { out := new(wrapperspb.BoolValue) err := c.cc.Invoke(ctx, Bridge_IsTLSCertificateInstalled_FullMethodName, in, out, opts...) @@ -858,10 +824,6 @@ type BridgeServer interface { LogoutUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) RemoveUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error) - // Telemetry - ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) - AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) - ExternalLinkClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) // TLS certificate related calls IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) InstallTLSCertificate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) @@ -1043,15 +1005,6 @@ func (UnimplementedBridgeServer) RemoveUser(context.Context, *wrapperspb.StringV func (UnimplementedBridgeServer) ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method ConfigureUserAppleMail not implemented") } -func (UnimplementedBridgeServer) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method ReportBugClicked not implemented") -} -func (UnimplementedBridgeServer) AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method AutoconfigClicked not implemented") -} -func (UnimplementedBridgeServer) ExternalLinkClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method ExternalLinkClicked not implemented") -} func (UnimplementedBridgeServer) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) { return nil, status.Errorf(codes.Unimplemented, "method IsTLSCertificateInstalled not implemented") } @@ -2073,60 +2026,6 @@ func _Bridge_ConfigureUserAppleMail_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } -func _Bridge_ReportBugClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(emptypb.Empty) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(BridgeServer).ReportBugClicked(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Bridge_ReportBugClicked_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BridgeServer).ReportBugClicked(ctx, req.(*emptypb.Empty)) - } - return interceptor(ctx, in, info, handler) -} - -func _Bridge_AutoconfigClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(wrapperspb.StringValue) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(BridgeServer).AutoconfigClicked(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Bridge_AutoconfigClicked_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BridgeServer).AutoconfigClicked(ctx, req.(*wrapperspb.StringValue)) - } - return interceptor(ctx, in, info, handler) -} - -func _Bridge_ExternalLinkClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(wrapperspb.StringValue) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(BridgeServer).ExternalLinkClicked(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: Bridge_ExternalLinkClicked_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(BridgeServer).ExternalLinkClicked(ctx, req.(*wrapperspb.StringValue)) - } - return interceptor(ctx, in, info, handler) -} - func _Bridge_IsTLSCertificateInstalled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -2465,18 +2364,6 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{ MethodName: "ConfigureUserAppleMail", Handler: _Bridge_ConfigureUserAppleMail_Handler, }, - { - MethodName: "ReportBugClicked", - Handler: _Bridge_ReportBugClicked_Handler, - }, - { - MethodName: "AutoconfigClicked", - Handler: _Bridge_AutoconfigClicked_Handler, - }, - { - MethodName: "ExternalLinkClicked", - Handler: _Bridge_ExternalLinkClicked_Handler, - }, { MethodName: "IsTLSCertificateInstalled", Handler: _Bridge_IsTLSCertificateInstalled_Handler, diff --git a/internal/frontend/grpc/service_telemetry.go b/internal/frontend/grpc/service_telemetry.go deleted file mode 100644 index d81107e9..00000000 --- a/internal/frontend/grpc/service_telemetry.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2024 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 . - -package grpc - -import ( - "context" - - "github.com/ProtonMail/gluon/async" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/wrapperspb" -) - -func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { - defer async.HandlePanic(s.panicHandler) - s.bridge.ReportBugClicked() - return &emptypb.Empty{}, nil -} - -func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) { - defer async.HandlePanic(s.panicHandler) - s.bridge.AutoconfigUsed(client.Value) - return &emptypb.Empty{}, nil -} - -func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) { - defer async.HandlePanic(s.panicHandler) - s.bridge.ExternalLinkClicked(article.Value) - return &emptypb.Empty{}, nil -} diff --git a/internal/locations/locations.go b/internal/locations/locations.go index f59f0ec0..1c49bb1a 100644 --- a/internal/locations/locations.go +++ b/internal/locations/locations.go @@ -188,16 +188,6 @@ func (l *Locations) ProvideUpdatesPath() (string, error) { return l.getUpdatesPath(), nil } -// ProvideStatsPath returns a location for statistics files (e.g. ~/.local/share///stats). -// It creates it if it doesn't already exist. -func (l *Locations) ProvideStatsPath() (string, error) { - if err := os.MkdirAll(l.getStatsPath(), 0o700); err != nil { - return "", err - } - - return l.getStatsPath(), nil -} - func (l *Locations) ProvideIMAPSyncConfigPath() (string, error) { if err := os.MkdirAll(l.getIMAPSyncConfigPath(), 0o700); err != nil { return "", err @@ -252,10 +242,6 @@ func (l *Locations) getNotificationsCachePath() string { return filepath.Join(l.userCache, "notifications") } -func (l *Locations) getStatsPath() string { - return filepath.Join(l.userData, "stats") -} - func (l *Locations) getUnleashCachePath() string { return filepath.Join(l.userCache, "unleash_cache") } // Clear removes everything except the lock and update files. diff --git a/internal/services/imapservice/connector.go b/internal/services/imapservice/connector.go index 04cc5aea..31ab8df4 100644 --- a/internal/services/imapservice/connector.go +++ b/internal/services/imapservice/connector.go @@ -56,7 +56,6 @@ type Connector struct { identityState sharedIdentity client APIClient - telemetry Telemetry reporter reporter.Reporter panicHandler async.PanicHandler sendRecorder *sendrecorder.SendRecorder @@ -80,7 +79,6 @@ func NewConnector( addressMode usertypes.AddressMode, sendRecorder *sendrecorder.SendRecorder, panicHandler async.PanicHandler, - telemetry Telemetry, reporter reporter.Reporter, showAllMail bool, syncState *SyncState, @@ -96,7 +94,6 @@ func NewConnector( attrs: defaultMailboxAttributes(), client: apiClient, - telemetry: telemetry, reporter: reporter, panicHandler: panicHandler, sendRecorder: sendRecorder, @@ -169,10 +166,9 @@ func (s *Connector) Init(ctx context.Context, cache connector.IMAPState) error { }) } -func (s *Connector) Authorize(ctx context.Context, username string, password []byte) bool { +func (s *Connector) Authorize(_ context.Context, username string, password []byte) bool { addrID, err := s.identityState.CheckAuth(username, password) if err != nil { - s.telemetry.ReportConfigStatusFailure("IMAP " + err.Error()) return false } @@ -180,8 +176,6 @@ func (s *Connector) Authorize(ctx context.Context, username string, password []b return false } - s.telemetry.SendConfigStatusSuccess(ctx) - return true } diff --git a/internal/services/imapservice/service.go b/internal/services/imapservice/service.go index 3ce0d63e..989b78fb 100644 --- a/internal/services/imapservice/service.go +++ b/internal/services/imapservice/service.go @@ -47,12 +47,6 @@ type EventProvider interface { RewindEventID(ctx context.Context, eventID string) error } -type Telemetry interface { - useridentity.Telemetry - SendConfigStatusSuccess(ctx context.Context) - ReportConfigStatusFailure(errDetails string) -} - type GluonIDProvider interface { GetGluonID(addrID string) (string, bool) GetGluonIDs() map[string]string @@ -77,7 +71,6 @@ type Service struct { serverManager IMAPServerManager eventPublisher events.EventPublisher - telemetry Telemetry panicHandler async.PanicHandler sendRecorder *sendrecorder.SendRecorder reporter reporter.Reporter @@ -112,7 +105,6 @@ func NewService( keyPassProvider useridentity.KeyPassProvider, panicHandler async.PanicHandler, sendRecorder *sendrecorder.SendRecorder, - telemetry Telemetry, reporter reporter.Reporter, addressMode usertypes.AddressMode, subscription events.Subscription, @@ -150,7 +142,6 @@ func NewService( panicHandler: panicHandler, sendRecorder: sendRecorder, - telemetry: telemetry, reporter: reporter, connectors: make(map[string]*Connector), @@ -513,7 +504,6 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) { s.addressMode, s.sendRecorder, s.panicHandler, - s.telemetry, s.reporter, s.showAllMail, s.syncStateProvider, @@ -531,7 +521,6 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) { s.addressMode, s.sendRecorder, s.panicHandler, - s.telemetry, s.reporter, s.showAllMail, s.syncStateProvider, diff --git a/internal/services/imapservice/service_address_events.go b/internal/services/imapservice/service_address_events.go index 6dfa8b94..163c5b65 100644 --- a/internal/services/imapservice/service_address_events.go +++ b/internal/services/imapservice/service_address_events.go @@ -154,7 +154,6 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro s.addressMode, s.sendRecorder, s.panicHandler, - s.telemetry, s.reporter, s.showAllMail, s.syncStateProvider, diff --git a/internal/services/smtp/accounts.go b/internal/services/smtp/accounts.go index f98d1d01..6a985053 100644 --- a/internal/services/smtp/accounts.go +++ b/internal/services/smtp/accounts.go @@ -66,14 +66,9 @@ func (s *Accounts) CheckAuth(user string, password []byte) (string, string, erro continue } - account.service.telemetry.ReportSMTPAuthSuccess(context.Background()) return id, addrID, nil } - for _, service := range s.accounts { - service.service.telemetry.ReportSMTPAuthFailed(user) - } - return "", "", ErrNoSuchUser } diff --git a/internal/services/smtp/service.go b/internal/services/smtp/service.go index e2f10649..be969201 100644 --- a/internal/services/smtp/service.go +++ b/internal/services/smtp/service.go @@ -39,12 +39,6 @@ import ( "github.com/sirupsen/logrus" ) -type Telemetry interface { - useridentity.Telemetry - ReportSMTPAuthSuccess(context.Context) - ReportSMTPAuthFailed(username string) -} - type Service struct { userID string panicHandler async.PanicHandler @@ -57,7 +51,6 @@ type Service struct { bridgePassProvider useridentity.BridgePassProvider keyPassProvider useridentity.KeyPassProvider identityState *useridentity.State - telemetry Telemetry eventService userevents.Subscribable subscription *userevents.EventChanneledSubscriber @@ -76,7 +69,6 @@ func NewService( reporter reporter.Reporter, bridgePassProvider useridentity.BridgePassProvider, keyPassProvider useridentity.KeyPassProvider, - telemetry Telemetry, eventService userevents.Subscribable, mode usertypes.AddressMode, identityState *useridentity.State, @@ -99,7 +91,6 @@ func NewService( bridgePassProvider: bridgePassProvider, keyPassProvider: keyPassProvider, - telemetry: telemetry, identityState: identityState, eventService: eventService, diff --git a/internal/services/useridentity/mocks/mocks.go b/internal/services/useridentity/mocks/mocks.go index 7c342639..a4655013 100644 --- a/internal/services/useridentity/mocks/mocks.go +++ b/internal/services/useridentity/mocks/mocks.go @@ -64,38 +64,3 @@ func (mr *MockIdentityProviderMockRecorder) GetUser(arg0 interface{}) *gomock.Ca mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockIdentityProvider)(nil).GetUser), arg0) } - -// MockTelemetry is a mock of Telemetry interface. -type MockTelemetry struct { - ctrl *gomock.Controller - recorder *MockTelemetryMockRecorder -} - -// MockTelemetryMockRecorder is the mock recorder for MockTelemetry. -type MockTelemetryMockRecorder struct { - mock *MockTelemetry -} - -// NewMockTelemetry creates a new mock instance. -func NewMockTelemetry(ctrl *gomock.Controller) *MockTelemetry { - mock := &MockTelemetry{ctrl: ctrl} - mock.recorder = &MockTelemetryMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTelemetry) EXPECT() *MockTelemetryMockRecorder { - return m.recorder -} - -// ReportConfigStatusFailure mocks base method. -func (m *MockTelemetry) ReportConfigStatusFailure(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "ReportConfigStatusFailure", arg0) -} - -// ReportConfigStatusFailure indicates an expected call of ReportConfigStatusFailure. -func (mr *MockTelemetryMockRecorder) ReportConfigStatusFailure(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportConfigStatusFailure", reflect.TypeOf((*MockTelemetry)(nil).ReportConfigStatusFailure), arg0) -} diff --git a/internal/services/useridentity/service.go b/internal/services/useridentity/service.go index 7e916da8..b6f4e693 100644 --- a/internal/services/useridentity/service.go +++ b/internal/services/useridentity/service.go @@ -50,7 +50,6 @@ type Service struct { subscription *userevents.EventChanneledSubscriber bridgePassProvider BridgePassProvider - telemetry Telemetry } func NewService( @@ -58,7 +57,6 @@ func NewService( eventPublisher events.EventPublisher, state *State, bridgePassProvider BridgePassProvider, - telemetry Telemetry, ) *Service { subscriberName := fmt.Sprintf("identity-%v", state.User.ID) @@ -73,7 +71,6 @@ func NewService( }), subscription: userevents.NewEventSubscriber(subscriberName), bridgePassProvider: bridgePassProvider, - telemetry: telemetry, } } diff --git a/internal/services/useridentity/service_test.go b/internal/services/useridentity/service_test.go index c2b43df3..bfbdcca0 100644 --- a/internal/services/useridentity/service_test.go +++ b/internal/services/useridentity/service_test.go @@ -361,10 +361,9 @@ func newTestService(_ *testing.T, mockCtrl *gomock.Controller) (*Service, *mocks eventPublisher := mocks2.NewMockEventPublisher(mockCtrl) provider := mocks.NewMockIdentityProvider(mockCtrl) user := newTestUser() - telemetry := mocks.NewMockTelemetry(mockCtrl) bridgePassProvider := NewFixedBridgePassProvider([]byte("hello")) - service := NewService(subscribable, eventPublisher, NewState(*user, newTestAddresses(), provider), bridgePassProvider, telemetry) + service := NewService(subscribable, eventPublisher, NewState(*user, newTestAddresses(), provider), bridgePassProvider) return service, eventPublisher, provider } diff --git a/internal/services/useridentity/telemetry.go b/internal/services/useridentity/telemetry.go deleted file mode 100644 index 06f1fcc8..00000000 --- a/internal/services/useridentity/telemetry.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2024 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 . - -package useridentity - -type Telemetry interface { - ReportConfigStatusFailure(errDetails string) -} diff --git a/internal/user/config_status.go b/internal/user/config_status.go deleted file mode 100644 index b5b7164c..00000000 --- a/internal/user/config_status.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2024 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 . - -package user - -import ( - "context" - "encoding/json" - "errors" - - "github.com/ProtonMail/gluon/reporter" - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/ProtonMail/proton-bridge/v3/internal/kb" -) - -func (user *User) SendConfigStatusSuccess(ctx context.Context) { - if user.configStatus.IsFromFailure() { - user.SendConfigStatusRecovery(ctx) - return - } - if !user.IsTelemetryEnabled(ctx) { - return - } - if !user.configStatus.IsPending() { - return - } - - var builder configstatus.ConfigSuccessBuilder - success := builder.New(user.configStatus) - data, err := json.Marshal(success) - if err != nil { - if err := user.reporter.ReportMessageWithContext("Cannot parse config_success data.", reporter.Context{ - "error": err, - }); err != nil { - user.log.WithError(err).Error("Failed to report config_success data parsing error.") - } - return - } - - if err := user.SendTelemetry(ctx, data); err == nil { - user.log.Info("Configuration Status Success event sent.") - if err := user.configStatus.ApplySuccess(); err != nil { - user.log.WithError(err).Error("Failed to ApplySuccess on config_status.") - } - } -} - -func (user *User) SendConfigStatusAbort(ctx context.Context, withTelemetry bool) { - if err := user.configStatus.Remove(); err != nil { - user.log.WithError(err).Error("Failed to remove config_status file.") - } - - if !user.configStatus.IsPending() { - return - } - if !withTelemetry || !user.IsTelemetryEnabled(ctx) { - return - } - var builder configstatus.ConfigAbortBuilder - abort := builder.New(user.configStatus) - data, err := json.Marshal(abort) - if err != nil { - if err := user.reporter.ReportMessageWithContext("Cannot parse config_abort data.", reporter.Context{ - "error": err, - }); err != nil { - user.log.WithError(err).Error("Failed to report config_abort data parsing error.") - } - return - } - - if err := user.SendTelemetry(ctx, data); err == nil { - user.log.Info("Configuration Status Abort event sent.") - } -} - -func (user *User) SendConfigStatusRecovery(ctx context.Context) { - if !user.configStatus.IsFromFailure() { - user.SendConfigStatusSuccess(ctx) - return - } - if !user.IsTelemetryEnabled(ctx) { - return - } - if !user.configStatus.IsPending() { - return - } - - var builder configstatus.ConfigRecoveryBuilder - success := builder.New(user.configStatus) - data, err := json.Marshal(success) - if err != nil { - if err := user.reporter.ReportMessageWithContext("Cannot parse config_recovery data.", reporter.Context{ - "error": err, - }); err != nil { - user.log.WithError(err).Error("Failed to report config_recovery data parsing error.") - } - return - } - - if err := user.SendTelemetry(ctx, data); err == nil { - user.log.Info("Configuration Status Recovery event sent.") - if err := user.configStatus.ApplySuccess(); err != nil { - user.log.WithError(err).Error("Failed to ApplySuccess on config_status.") - } - } -} - -func (user *User) SendConfigStatusProgress(ctx context.Context) { - if !user.IsTelemetryEnabled(ctx) { - return - } - if !user.configStatus.IsPending() { - return - } - var builder configstatus.ConfigProgressBuilder - progress := builder.New(user.configStatus) - if progress.Values.NbDay == 0 { - return - } - if progress.Values.NbDaySinceLast == 0 { - return - } - - data, err := json.Marshal(progress) - if err != nil { - if err := user.reporter.ReportMessageWithContext("Cannot parse config_progress data.", reporter.Context{ - "error": err, - }); err != nil { - user.log.WithError(err).Error("Failed to report config_progress data parsing error.") - } - return - } - - if err := user.SendTelemetry(ctx, data); err == nil { - user.log.Info("Configuration Status Progress event sent.") - if err := user.configStatus.ApplyProgress(); err != nil { - user.log.WithError(err).Error("Failed to ApplyProgress on config_status.") - } - } -} - -func (user *User) ReportConfigStatusFailure(errDetails string) { - if user.configStatus.IsPending() { - return - } - - if err := user.configStatus.ApplyFailure(errDetails); err != nil { - user.log.WithError(err).Error("Failed to ApplyFailure on config_status.") - } else { - user.log.Info("Configuration Status is back to Pending due to Failure.") - } -} - -func (user *User) ReportBugClicked() { - if !user.configStatus.IsPending() { - return - } - - if err := user.configStatus.ReportClicked(); err != nil { - user.log.WithError(err).Error("Failed to log ReportClicked in config_status.") - } -} - -func (user *User) ReportBugSent() { - if !user.configStatus.IsPending() { - return - } - - if err := user.configStatus.ReportSent(); err != nil { - user.log.WithError(err).Error("Failed to log ReportSent in config_status.") - } -} - -func (user *User) AutoconfigUsed(client string) { - if !user.configStatus.IsPending() { - return - } - - if err := user.configStatus.AutoconfigUsed(client); err != nil { - user.log.WithError(err).Error("Failed to log Autoconf in config_status.") - } -} - -func (user *User) ExternalLinkClicked(url string) { - if !user.configStatus.IsPending() { - return - } - - const externalLinkWasClicked = "External link was clicked." - index, err := kb.GetArticleIndex(url) - if err != nil { - if errors.Is(err, kb.ErrArticleNotFound) { - user.log.WithField("report", false).WithField("url", url).Debug(externalLinkWasClicked) - } else { - user.log.WithError(err).Error("Failed to retrieve list of KB articles.") - } - return - } - - if err := user.configStatus.RecordLinkClicked(index); err != nil { - user.log.WithError(err).Error("Failed to log LinkClicked in config_status.") - } else { - user.log.WithField("report", true).WithField("url", url).Debug(externalLinkWasClicked) - } -} diff --git a/internal/user/user.go b/internal/user/user.go index 6e1ce73f..b9c1170c 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -21,13 +21,11 @@ import ( "context" "encoding/json" "fmt" - "path/filepath" "time" "github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/go-proton-api" - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" @@ -78,10 +76,7 @@ type User struct { maxSyncMemory uint64 panicHandler async.PanicHandler - configStatus *configstatus.ConfigurationStatus telemetryManager telemetry.Availability - // goStatusProgress triggers a check/sending if progress is needed. - goStatusProgress func() eventService *userevents.Service identityService *useridentity.Service @@ -104,7 +99,6 @@ func New( crashHandler async.PanicHandler, showAllMail bool, maxSyncMemory uint64, - statsDir string, telemetryManager telemetry.Availability, imapServerManager imapservice.IMAPServerManager, smtpServerManager smtp.ServerManager, @@ -125,7 +119,6 @@ func New( crashHandler, showAllMail, maxSyncMemory, - statsDir, telemetryManager, imapServerManager, smtpServerManager, @@ -159,7 +152,6 @@ func newImpl( crashHandler async.PanicHandler, showAllMail bool, maxSyncMemory uint64, - statsDir string, telemetryManager telemetry.Availability, imapServerManager imapservice.IMAPServerManager, smtpServerManager smtp.ServerManager, @@ -198,12 +190,6 @@ func newImpl( "numLabels": len(apiLabels), }).Info("Creating user object") - configStatusFile := filepath.Join(statsDir, apiUser.ID+".json") - configStatus, err := configstatus.LoadConfigurationStatus(configStatusFile) - if err != nil { - return nil, fmt.Errorf("failed to init configuration status file: %w", err) - } - sendRecorder := sendrecorder.NewSendRecorder(sendrecorder.SendEntryExpiry) // Create the user object. @@ -225,7 +211,6 @@ func newImpl( panicHandler: crashHandler, - configStatus: configStatus, telemetryManager: telemetryManager, serviceGroup: orderedtasks.NewOrderedCancelGroup(crashHandler), @@ -248,7 +233,7 @@ func newImpl( addressMode := usertypes.VaultToAddressMode(encVault.AddressMode()) - user.identityService = useridentity.NewService(user.eventService, user, identityState, encVault, user) + user.identityService = useridentity.NewService(user.eventService, user, identityState, encVault) user.telemetryService = telemetryservice.NewService(apiUser.ID, client, user.eventService) @@ -260,7 +245,6 @@ func newImpl( reporter, encVault, encVault, - user, user.eventService, addressMode, identityState.Clone(), @@ -279,7 +263,6 @@ func newImpl( encVault, crashHandler, sendRecorder, - user, reporter, addressMode, eventSubscription, @@ -291,12 +274,6 @@ func newImpl( user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService) - // Check for status_progress when triggered. - user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) { - user.SendConfigStatusProgress(ctx) - }) - defer user.goStatusProgress() - // When we receive an auth object, we update it in the vault. // This will be used to authorize the user on the next run. user.client.AddAuthHandler(func(auth proton.Auth) { @@ -698,19 +675,6 @@ func (user *User) SendTelemetry(ctx context.Context, data []byte) error { return nil } -func (user *User) ReportSMTPAuthFailed(username string) { - emails := user.Emails() - for _, mail := range emails { - if mail == username { - user.ReportConfigStatusFailure("SMTP invalid username or password") - } - } -} - -func (user *User) ReportSMTPAuthSuccess(ctx context.Context) { - user.SendConfigStatusSuccess(ctx) -} - func (user *User) GetSMTPService() *smtp.Service { return user.smtpService } diff --git a/internal/user/user_test.go b/internal/user/user_test.go index 64ccab9b..5cbc265a 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -160,7 +160,6 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma nil, true, vault.DefaultMaxSyncMemory, - tb.TempDir(), manager, nullIMAPServerManager, nullSMTPServerManager, diff --git a/tests/config_status_test.go b/tests/config_status_test.go deleted file mode 100644 index fef673e9..00000000 --- a/tests/config_status_test.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2024 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 . - -package tests - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/ProtonMail/go-proton-api" - "github.com/ProtonMail/go-proton-api/server" - "github.com/ProtonMail/proton-bridge/v3/internal/configstatus" - "github.com/sirupsen/logrus" -) - -func (s *scenario) configStatusFileExistForUser(username string) error { - configStatusFile, err := getConfigStatusFile(s.t, username) - if err != nil { - return err - } - if _, err := os.Stat(configStatusFile); err != nil { - return err - } - return nil -} - -func (s *scenario) configStatusIsPendingForUser(username string) error { - data, err := loadConfigStatusFile(s.t, username) - if err != nil { - return err - } - - if data.DataV1.PendingSince.IsZero() { - return fmt.Errorf("expected ConfigStatus pending but got success instead") - } - - return nil -} - -func (s *scenario) configStatusIsPendingWithFailureForUser(username string) error { - data, err := loadConfigStatusFile(s.t, username) - if err != nil { - return err - } - - if data.DataV1.PendingSince.IsZero() { - return fmt.Errorf("expected ConfigStatus pending but got success instead") - } - if data.DataV1.FailureDetails == "" { - return fmt.Errorf("expected ConfigStatus pending with failure but got no failure instead") - } - - return nil -} - -func (s *scenario) configStatusSucceedForUser(username string) error { - data, err := loadConfigStatusFile(s.t, username) - if err != nil { - return err - } - - if !data.DataV1.PendingSince.IsZero() { - return fmt.Errorf("expected ConfigStatus success but got pending since %s", data.DataV1.PendingSince) - } - - return nil -} - -func (s *scenario) configStatusEventIsEventuallySendXTime(event string, number int) error { - return eventually(func() error { - err := s.checkEventSentForUser(event, number) - logrus.WithError(err).Trace("Matching eventually") - return err - }) -} - -func (s *scenario) configStatusEventIsNotSendMoreThanXTime(event string, number int) error { - if err := eventually(func() error { - err := s.checkEventSentForUser(event, number+1) - logrus.WithError(err).Trace("Matching eventually") - return err - }); err == nil { - return fmt.Errorf("expected %s to be sent %d but catch %d", event, number, number+1) - } - return nil -} - -func (s *scenario) forceConfigStatusProgressToBeSentForUser(username string) error { - configStatusFile, err := getConfigStatusFile(s.t, username) - if err != nil { - return err - } - - data, err := loadConfigStatusFile(s.t, username) - if err != nil { - return err - } - data.DataV1.PendingSince = time.Now().AddDate(0, 0, -2) - data.DataV1.LastProgress = time.Now().AddDate(0, 0, -1) - - f, err := os.Create(configStatusFile) - if err != nil { - return err - } - defer func() { _ = f.Close() }() - - return json.NewEncoder(f).Encode(data) -} - -func (s *scenario) checkEventSentForUser(event string, number int) error { - calls, err := getLastTelemetryEventSent(s.t, event) - if err != nil { - return err - } - if len(calls) != number { - return fmt.Errorf("expected %s to be sent %d but catch %d", event, number, len(calls)) - } - return nil -} - -func getConfigStatusFile(t *testCtx, username string) (string, error) { - userID := t.getUserByName(username).getUserID() - statsDir, err := t.locator.ProvideStatsPath() - if err != nil { - return "", fmt.Errorf("failed to get Statistics directory: %w", err) - } - return filepath.Join(statsDir, userID+".json"), nil -} - -func loadConfigStatusFile(t *testCtx, username string) (configstatus.ConfigurationStatusData, error) { - data := configstatus.ConfigurationStatusData{} - - configStatusFile, err := getConfigStatusFile(t, username) - if err != nil { - return data, err - } - - if _, err := os.Stat(configStatusFile); err != nil { - return data, err - } - - f, err := os.Open(configStatusFile) - if err != nil { - return data, err - } - defer func() { _ = f.Close() }() - - err = json.NewDecoder(f).Decode(&data) - return data, err -} - -func getLastTelemetryEventSent(t *testCtx, event string) ([]server.Call, error) { - var matches []server.Call - - calls, err := t.getAllCalls("POST", "/data/v1/stats") - if err != nil { - return matches, err - } - - for _, call := range calls { - var req proton.SendStatsReq - if err := json.Unmarshal(call.RequestBody, &req); err != nil { - continue - } - if req.Event == event { - matches = append(matches, call) - } - } - return matches, err -} diff --git a/tests/features/bridge/config_status.feature b/tests/features/bridge/config_status.feature deleted file mode 100644 index 85e7b5bd..00000000 --- a/tests/features/bridge/config_status.feature +++ /dev/null @@ -1,79 +0,0 @@ -Feature: Configuration Status Telemetry - Background: - Given there exists an account with username "[user:user]" and password "password" - Then it succeeds - When bridge starts - Then it succeeds - - - Scenario: Init config status on user addition - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - Then config status file exist for user "[user:user]" - And config status is pending for user "[user:user]" - - - Scenario: Config Status Success on IMAP - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - Then config status file exist for user "[user:user]" - And config status is pending for user "[user:user]" - When user "[user:user]" connects and authenticates IMAP client "1" - Then config status succeed for user "[user:user]" - And config status event "bridge_config_success" is eventually send 1 time - - - Scenario: Config Status Success on SMTP - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - Then config status file exist for user "[user:user]" - And config status is pending for user "[user:user]" - When user "[user:user]" connects and authenticates SMTP client "1" - Then config status succeed for user "[user:user]" - And config status event "bridge_config_success" is eventually send 1 time - - - Scenario: Config Status Success send only once - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - Then config status file exist for user "[user:user]" - And config status is pending for user "[user:user]" - When user "[user:user]" connects and authenticates IMAP client "1" - Then config status succeed for user "[user:user]" - And config status event "bridge_config_success" is eventually send 1 time - When user "[user:user]" connects and authenticates IMAP client "2" - Then config status event "bridge_config_success" is not send more than 1 time - - - Scenario: Config Status Abort - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - And user "[user:user]" finishes syncing - Then config status file exist for user "[user:user]" - And config status is pending for user "[user:user]" - When user "[user:user]" is deleted - Then config status event "bridge_config_abort" is eventually send 1 time - - - Scenario: Config Status Recovery from deauth - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - And user "[user:user]" connects and authenticates IMAP client "1" - Then config status succeed for user "[user:user]" - When the auth of user "[user:user]" is revoked - Then bridge sends a deauth event for user "[user:user]" - Then config status is pending with failure for user "[user:user]" - When the user logs in with username "[user:user]" and password "password" - And user "[user:user]" connects and authenticates IMAP client "1" - Then config status succeed for user "[user:user]" - And config status event "bridge_config_recovery" is eventually send 1 time - - - Scenario: Config Status Progress - Then bridge telemetry feature is enabled - When the user logs in with username "[user:user]" and password "password" - And config status is pending for user "[user:user]" - And bridge stops - And force config status progress to be sent for user"[user:user]" - And bridge starts - Then config status event "bridge_config_progress" is eventually send 1 time diff --git a/tests/steps_test.go b/tests/steps_test.go index 58bf1358..470a2a0e 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -203,13 +203,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat) ctx.Step(`^bridge do not need to send heartbeat`, s.bridgeDoNotNeedToSendHeartbeat) ctx.Step(`^heartbeat is not whitelisted`, s.heartbeatIsNotwhitelisted) - ctx.Step(`^config status file exist for user "([^"]*)"$`, s.configStatusFileExistForUser) - ctx.Step(`^config status is pending for user "([^"]*)"$`, s.configStatusIsPendingForUser) - ctx.Step(`^config status is pending with failure for user "([^"]*)"$`, s.configStatusIsPendingWithFailureForUser) - ctx.Step(`^config status succeed for user "([^"]*)"$`, s.configStatusSucceedForUser) - ctx.Step(`^config status event "([^"]*)" is eventually send (\d+) time`, s.configStatusEventIsEventuallySendXTime) - ctx.Step(`^config status event "([^"]*)" is not send more than (\d+) time`, s.configStatusEventIsNotSendMoreThanXTime) - ctx.Step(`^force config status progress to be sent for user"([^"]*)"$`, s.forceConfigStatusProgressToBeSentForUser) // ==== CONTACT ==== ctx.Step(`^user "([^"]*)" has contact "([^"]*)" with name "([^"]*)"$`, s.userHasContactWithName) From f1aef383b75710ad6990b0eccfbfa94d2992026f Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Tue, 5 Nov 2024 16:45:15 +0100 Subject: [PATCH 03/19] fix(BRIDGE-258): fixed issue with draft updates and sending during synchronization --- .../services/imapservice/service_sync_events.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/services/imapservice/service_sync_events.go b/internal/services/imapservice/service_sync_events.go index 42905f75..eb9a2c84 100644 --- a/internal/services/imapservice/service_sync_events.go +++ b/internal/services/imapservice/service_sync_events.go @@ -65,6 +65,22 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events return err } + case proton.EventUpdate: + if event.Message.IsDraft() || (event.Message.Flags&proton.MessageFlagSent != 0) { + updates, err := onMessageUpdateDraftOrSent( + logging.WithLogrusField(ctx, "action", "update draft or sent message (sync)"), + s.service, + event, + ) + if err != nil { + return fmt.Errorf("failed to handle update draft event (sync): %w", err) + } + + if err := waitOnIMAPUpdates(ctx, updates); err != nil { + return err + } + } + case proton.EventDelete: updates := onMessageDeleted( logging.WithLogrusField(ctx, "action", "delete message (sync)"), From 7d9753e2daf44fc760fc1b6739371a11878a7a89 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Wed, 6 Nov 2024 16:02:05 +0100 Subject: [PATCH 04/19] fix(BRIDGE-107): improved human verification UX --- internal/bridge/bridge.go | 10 ++++++++++ .../bridge-gui/bridge-gui-tester/GRPCService.cpp | 2 +- .../bridge-gui/bridge-gui/qml/SetupWizard/Login.qml | 7 ++++++- internal/frontend/cli/accounts.go | 5 ++++- internal/frontend/grpc/service.go | 7 +++++-- internal/frontend/grpc/service_methods.go | 3 ++- internal/hv/hv.go | 7 ++++++- 7 files changed, 34 insertions(+), 7 deletions(-) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 63387292..88ef7676 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -723,3 +723,13 @@ func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.Dis func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) { bridge.observabilityService.ModifyHeartbeatInterval(duration) } + +func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx reporter.Context) { + if err := bridge.reporter.ReportMessageWithContext(message, messageCtx); err != nil { + logPkg.WithFields(logrus.Fields{ + "err": err, + "sentryMessage": message, + "messageCtx": messageCtx, + }).Info("Error occurred when sending Report to Sentry") + } +} diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp index 9eb0fac2..590abeb7 100644 --- a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp +++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp @@ -30,7 +30,7 @@ using namespace bridgepp; namespace { QString const defaultKeychain = "defaultKeychain"; ///< The default keychain. -QString const HV_ERROR_TEMPLATE = "failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: CAPTCHA validation failed (Code=12087, Status=422)"; +QString const HV_ERROR_TEMPLATE = "Human verification failed. Please try again."; } diff --git a/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/Login.qml b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/Login.qml index 2f1a86a9..c16529a4 100644 --- a/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/Login.qml +++ b/internal/frontend/bridge-gui/bridge-gui/qml/SetupWizard/Login.qml @@ -29,6 +29,7 @@ FocusScope { property alias username: usernameTextField.text property var wizard property string hvLinkUrl: "" + property bool hvLinkClicked: false signal loginAbort(string username, bool wasSignedOut) @@ -49,6 +50,7 @@ FocusScope { } passwordTextField.hidePassword(); secondPasswordTextField.hidePassword(); + hvLinkClicked = false; } function resetViaHv() { usernameTextField.enabled = false; @@ -56,6 +58,7 @@ FocusScope { signInButton.loading = true; secondPasswordButton.loading = false; secondPasswordTextField.enabled = true; + hvLinkClicked = false; totpLayout.reset(); } @@ -562,6 +565,7 @@ FocusScope { cursorShape: Qt.PointingHandCursor onClicked: { Qt.openUrlExternally(hvLinkUrl); + hvLinkClicked = true; } } } @@ -574,7 +578,8 @@ FocusScope { id: hVContinueButton Layout.fillWidth: true colorScheme: wizard.colorScheme - text: qsTr("Continue") + text: qsTr("I’ve completed the verification") + enabled: hvLinkClicked function checkAndSignInHv() { console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv") diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index 6dcaf440..2e8afb28 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -159,7 +159,10 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err) if hvErr != nil || hvDetails != nil { if hvErr != nil { - f.printAndLogError("Cannot login", hvErr) + f.printAndLogError("Cannot login:", hv.ExtractionErrorMsg) + f.bridge.ReportMessageWithContext("Unable to extract HV request details", map[string]any{ + "error": err.Error(), + }) return } f.promptHvURL(hvDetails) diff --git a/internal/frontend/grpc/service.go b/internal/frontend/grpc/service.go index 901fe61b..3b1b12d3 100644 --- a/internal/frontend/grpc/service.go +++ b/internal/frontend/grpc/service.go @@ -465,7 +465,7 @@ func (s *Service) finishLogin() { if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.HumanValidationInvalidToken { s.hvDetails = nil - _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error())) + _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hv.VerificationFailedErrorMsg)) return } @@ -643,7 +643,10 @@ func (s *Service) monitorParentPID() { func (s *Service) handleHvRequest(err error) { hvDet, hvErr := hv.VerifyAndExtractHvRequest(err) if hvErr != nil { - _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hvErr.Error())) + _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hv.ExtractionErrorMsg)) + s.bridge.ReportMessageWithContext("Unable to extract HV request details", map[string]any{ + "error": err.Error(), + }) return } diff --git a/internal/frontend/grpc/service_methods.go b/internal/frontend/grpc/service_methods.go index d59c675e..0faa3fa0 100644 --- a/internal/frontend/grpc/service_methods.go +++ b/internal/frontend/grpc/service_methods.go @@ -30,6 +30,7 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme" + "github.com/ProtonMail/proton-bridge/v3/internal/hv" "github.com/ProtonMail/proton-bridge/v3/internal/kb" "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/service" @@ -468,7 +469,7 @@ func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, case proton.HumanValidationInvalidToken: s.hvDetails = nil - _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error())) + _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hv.VerificationFailedErrorMsg)) default: _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error())) diff --git a/internal/hv/hv.go b/internal/hv/hv.go index 69e40bd6..5d312a51 100644 --- a/internal/hv/hv.go +++ b/internal/hv/hv.go @@ -21,6 +21,11 @@ import ( "github.com/ProtonMail/go-proton-api" ) +const ( + ExtractionErrorMsg = "Human verification requested, but an issue occurred. Please try again." + VerificationFailedErrorMsg = "Human verification failed. Please try again." +) + // VerifyAndExtractHvRequest expects an error request as input // determines whether the given error is a Proton human verification request; if it isn't then it returns -> nil, nil (no details, no error) // if it is a HV req. then it tries to parse the json data and verify that the captcha method is included; if either fails -> nil, err @@ -34,7 +39,7 @@ func VerifyAndExtractHvRequest(err error) (*proton.APIHVDetails, error) { if errors.As(err, &protonErr) && protonErr.IsHVError() { hvDetails, hvErr := protonErr.GetHVDetails() if hvErr != nil { - return nil, fmt.Errorf("received HV request, but can't decode HV details") + return nil, hvErr } return hvDetails, nil } From b3e2a91f563aa9cba2bf3ace467532fdbff16285 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Mon, 11 Nov 2024 08:21:44 +0100 Subject: [PATCH 05/19] feat(BRIDGE-205): add support for the IMAP AUTHENTICATE command. --- go.mod | 2 +- go.sum | 6 ++ tests/features/imap/auth.feature | 26 ++++++- tests/imap_test.go | 116 ++++++++++++++++++++++++++++--- tests/steps_test.go | 6 ++ 5 files changed, 144 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index aa8aac2a..9170f769 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.9 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e + github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton diff --git a/go.sum b/go.sum index e936d2d0..56ade6a1 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,12 @@ github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032 h1:5bwI+mwF26c github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e h1:+UfdKOkF9JEiH9VXWBo+/nlXNVSJcxtuf4+SJTrk9fw= github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087 h1:hqoJCo54y/4cO1w9ZfaqRMAvxdxJMRT0vc0ICbg8nVA= +github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c h1:+klUNkIb8TMXxnE80PDJM5YV2gPfmyOal3hiofdGSAs= +github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce h1:lphIROziz1jya/E40KzWSDNm+tEyp86XkPk7qk1LgVY= +github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= 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= diff --git a/tests/features/imap/auth.feature b/tests/features/imap/auth.feature index a4662e38..4ee4fc6e 100644 --- a/tests/features/imap/auth.feature +++ b/tests/features/imap/auth.feature @@ -13,26 +13,46 @@ Feature: A user can authenticate an IMAP client When user "[user:user]" connects IMAP client "1" Then IMAP client "1" can authenticate + Scenario: IMAP client can authenticate successfully using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" can authenticate using IMAP AUTHENTICATE + Scenario: IMAP client can authenticate successfully with different case When user "[user:user]" connects IMAP client "1" Then IMAP client "1" can authenticate with address "{toUpper:[user:user]@[domain]}" + Scenario: IMAP client can authenticate successfully with different case using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" can authenticate with address "{toUpper:[user:user]@[domain]}" using IMAP AUTHENTICATE + Scenario: IMAP client can authenticate successfully with secondary address Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" - Scenario: IMAP client can authenticate successfully - When user "[user:user]" connects IMAP client "1" - Then IMAP client "1" can authenticate + Scenario: IMAP client can authenticate successfully with secondary address using IMAP AUTHENTICATE + Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" using IMAP AUTHENTICATE Scenario: IMAP client cannot authenticate with bad username When user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate with incorrect username + Scenario: IMAP client cannot authenticate with bad username using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate with incorrect username using IMAP AUTHENTICATE + Scenario: IMAP client cannot authenticate with bad password When user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate with incorrect password + Scenario: IMAP client cannot authenticate with bad password using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate with incorrect password using IMAP AUTHENTICATE + Scenario: IMAP client cannot authenticate for disconnected user When user "[user:user]" logs out And user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate + + Scenario: IMAP client cannot authenticate using IMAP AUTHENTICATE for disconnected user + When user "[user:user]" logs out + And user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate using IMAP AUTHENTICATE diff --git a/tests/imap_test.go b/tests/imap_test.go index 8245e6f7..cd21fb3b 100644 --- a/tests/imap_test.go +++ b/tests/imap_test.go @@ -36,10 +36,38 @@ import ( "github.com/emersion/go-imap" id "github.com/emersion/go-imap-id" "github.com/emersion/go-imap/client" + "github.com/emersion/go-sasl" "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) +type imapAuthMethod int + +const ( + imapLogin imapAuthMethod = iota + imapAuthenticate +) + +func (s *scenario) loginWithAuthMethod(client *client.Client, username, password string, authMethod imapAuthMethod) error { + switch authMethod { + case imapLogin: + return client.Login(username, password) + case imapAuthenticate: + supported, err := client.SupportAuth(sasl.Plain) + if err != nil { + return err + } + + if !supported { + return errors.New("server does not support AUTHENTICATE PLAIN") + } + + return client.Authenticate(sasl.NewPlainClient("", username, password)) + default: + return errors.New("unknown IMAP auth method") + } +} + func (s *scenario) userConnectsIMAPClient(username, clientID string) error { return s.t.newIMAPClient(s.t.getUserByName(username).getUserID(), clientID) } @@ -59,7 +87,17 @@ func (s *scenario) userConnectsAndAuthenticatesIMAPClientWithAddress(username, c userID, client := s.t.getIMAPClient(clientID) - return client.Login(address, s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) userConnectsAndAuthenticatesIMAPClientWithAddressUsingIMAPAuthenticate(username, clientID, address string) error { + if err := s.t.newIMAPClient(s.t.getUserByName(username).getUserID(), clientID); err != nil { + return err + } + + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(username, clientID, address string) error { @@ -69,7 +107,7 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(address, s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin); err == nil { return fmt.Errorf("expected error, got nil") } @@ -79,19 +117,51 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna func (s *scenario) imapClientCanAuthenticate(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - return client.Login(s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) imapClientCanAuthenticateUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) imapClientCanAuthenticateWithAddress(clientID string, address string) error { userID, client := s.t.getIMAPClient(clientID) - return client.Login(address, s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) imapClientCanAuthenticateWithAddressUsingIMAPAuthenticate(clientID string, address string) error { + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) imapClientCannotAuthenticate(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0], + s.t.getUserByID(userID).getBridgePass(), + imapLogin, + ); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0], + s.t.getUserByID(userID).getBridgePass(), + imapAuthenticate, + ); err == nil { return fmt.Errorf("expected error, got nil") } @@ -101,7 +171,7 @@ func (s *scenario) imapClientCannotAuthenticate(clientID string) error { func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(address, s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin); err == nil { return fmt.Errorf("expected error, got nil") } @@ -111,7 +181,27 @@ func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address str func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0]+"bad", s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0]+"bad", + s.t.getUserByID(userID).getBridgePass(), + imapLogin, + ); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsernameUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0]+"bad", + s.t.getUserByID(userID).getBridgePass(), + imapAuthenticate, + ); err == nil { return fmt.Errorf("expected error, got nil") } @@ -121,7 +211,17 @@ func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID st func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID string) error { userID, client := s.t.getIMAPClient(clientID) badPass := base64.StdEncoding.EncodeToString([]byte("bad_password")) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0], badPass); err == nil { + if err := s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], badPass, imapLogin); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateWithIncorrectPasswordUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + badPass := base64.StdEncoding.EncodeToString([]byte("bad_password")) + if err := s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], badPass, imapAuthenticate); err == nil { return fmt.Errorf("expected error, got nil") } diff --git a/tests/steps_test.go b/tests/steps_test.go index 470a2a0e..12c8cfff 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -129,13 +129,19 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)" on port (\d+)$`, s.userConnectsIMAPClientOnPort) ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClient) ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)" with address "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClientWithAddress) + ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)" with address "([^"]*)" using IMAP AUTHENTICATE$`, s.userConnectsAndAuthenticatesIMAPClientWithAddressUsingIMAPAuthenticate) ctx.Step(`^user "([^"]*)" connects and can not authenticate IMAP client "([^"]*)" with address "([^"]*)"$`, s.userConnectsAndCanNotAuthenticateIMAPClientWithAddress) ctx.Step(`^IMAP client "([^"]*)" can authenticate$`, s.imapClientCanAuthenticate) + ctx.Step(`^IMAP client "([^"]*)" can authenticate using IMAP AUTHENTICATE$`, s.imapClientCanAuthenticateUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" can authenticate with address "([^"]*)"$`, s.imapClientCanAuthenticateWithAddress) + ctx.Step(`^IMAP client "([^"]*)" can authenticate with address "([^"]*)" using IMAP AUTHENTICATE$`, s.imapClientCanAuthenticateWithAddressUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate$`, s.imapClientCannotAuthenticate) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with address "([^"]*)"$`, s.imapClientCannotAuthenticateWithAddress) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username$`, s.imapClientCannotAuthenticateWithIncorrectUsername) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateWithIncorrectUsernameUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect password$`, s.imapClientCannotAuthenticateWithIncorrectPassword) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect password using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateWithIncorrectPasswordUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" closes$`, s.imapClientCloses) ctx.Step(`^IMAP client "([^"]*)" announces its ID with name "([^"]*)" and version "([^"]*)"$`, s.imapClientAnnouncesItsIDWithNameAndVersion) ctx.Step(`^IMAP client "([^"]*)" creates "([^"]*)"$`, s.imapClientCreatesMailbox) From cdcdd45bcf6410bb6bdeca7e362feba27aeccea9 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 22 Nov 2024 11:19:44 +0100 Subject: [PATCH 06/19] feat(BRIDGE-268): add kill switch feature flag for the IMAP AUTHENTICATE command. --- go.mod | 2 +- go.sum | 22 ++------------------- internal/bridge/imap.go | 5 +++++ internal/services/imapsmtpserver/imap.go | 12 +++++++++-- internal/services/imapsmtpserver/service.go | 1 + internal/services/notifications/service.go | 3 +-- internal/unleash/service.go | 5 +++++ 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 9170f769..99aa4b10 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.9 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce + github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton diff --git a/go.sum b/go.sum index 56ade6a1..9d5e7a20 100644 --- a/go.sum +++ b/go.sum @@ -34,26 +34,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= -github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc= -github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af h1:iMxTQUg2cB47cXqpMev3cZmQoGBOef3cSUjBbdEl33M= -github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060 h1:dcu3tT84GjoXb++n7crv8UJeG8eRwogjTYdkoJ+MjQI= -github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8 h1:YxPHSJUA87i1hc6s1YrW89++V7HpcR7LSFQ6XM0TsAE= -github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4 h1:xE+V17O9HIttMpVymNCORQILk9OKpSekrrPbX7YGnF8= -github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032 h1:5bwI+mwF26c460xlq2Dw3/cVF1cU4Xo4kTKX1/pBXko= -github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e h1:+UfdKOkF9JEiH9VXWBo+/nlXNVSJcxtuf4+SJTrk9fw= -github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087 h1:hqoJCo54y/4cO1w9ZfaqRMAvxdxJMRT0vc0ICbg8nVA= -github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c h1:+klUNkIb8TMXxnE80PDJM5YV2gPfmyOal3hiofdGSAs= -github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= -github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce h1:lphIROziz1jya/E40KzWSDNm+tEyp86XkPk7qk1LgVY= -github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2 h1:iZjKvjb6VkGb52ZaBBiXC1MGYJN4C/S97JfppdzpMHQ= +github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= 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= diff --git a/internal/bridge/imap.go b/internal/bridge/imap.go index 58b93d3c..40438ff3 100644 --- a/internal/bridge/imap.go +++ b/internal/bridge/imap.go @@ -26,6 +26,7 @@ import ( imapEvents "github.com/ProtonMail/gluon/events" "github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver" + "github.com/ProtonMail/proton-bridge/v3/internal/unleash" "github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/sirupsen/logrus" ) @@ -93,6 +94,10 @@ func (b *bridgeIMAPSettings) LogServer() bool { return b.b.logIMAPServer } +func (b *bridgeIMAPSettings) DisableIMAPAuthenticate() bool { + return b.b.unleashService.GetFlagValue(unleash.IMAPAuthenticateCommandDisabled) +} + func (b *bridgeIMAPSettings) Port() int { return b.b.vault.GetIMAPPort() } diff --git a/internal/services/imapsmtpserver/imap.go b/internal/services/imapsmtpserver/imap.go index 806b6c56..0ec98a3f 100644 --- a/internal/services/imapsmtpserver/imap.go +++ b/internal/services/imapsmtpserver/imap.go @@ -49,6 +49,7 @@ type IMAPSettingsProvider interface { Port() int SetPort(int) error UseSSL() bool + DisableIMAPAuthenticate() bool CacheDirectory() string DataDirectory() (string, error) SetCacheDirectory(string) error @@ -74,6 +75,7 @@ func newIMAPServer( tlsConfig *tls.Config, reporter reporter.Reporter, logClient, logServer bool, + disableIMAPAuthenticate bool, eventPublisher IMAPEventPublisher, tasks *async.Group, uidValidityGenerator imap.UIDValidityGenerator, @@ -113,7 +115,7 @@ func newIMAPServer( imapServerLog = io.Discard } - imapServer, err := gluon.New( + options := []gluon.Option{ gluon.WithTLS(tlsConfig), gluon.WithDataDir(gluonCacheDir), gluon.WithDatabaseDir(gluonConfigDir), @@ -124,7 +126,13 @@ func newIMAPServer( gluon.WithUIDValidityGenerator(uidValidityGenerator), gluon.WithPanicHandler(panicHandler), gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)), - ) + } + + if disableIMAPAuthenticate { + options = append(options, gluon.WithDisableIMAPAuthenticate()) + } + + imapServer, err := gluon.New(options...) if err != nil { return nil, err } diff --git a/internal/services/imapsmtpserver/service.go b/internal/services/imapsmtpserver/service.go index f56e7275..a80ddf7e 100644 --- a/internal/services/imapsmtpserver/service.go +++ b/internal/services/imapsmtpserver/service.go @@ -451,6 +451,7 @@ func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error) sm.reporter, sm.imapSettings.LogClient(), sm.imapSettings.LogServer(), + sm.imapSettings.DisableIMAPAuthenticate(), sm.imapSettings.EventPublisher(), sm.tasks, sm.uidValidityGenerator, diff --git a/internal/services/notifications/service.go b/internal/services/notifications/service.go index bf2bbc83..8a78f53e 100644 --- a/internal/services/notifications/service.go +++ b/internal/services/notifications/service.go @@ -50,7 +50,6 @@ type Service struct { } const bitfieldRegexPattern = `^\\\d+` -const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled" func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store, getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service { @@ -103,7 +102,7 @@ func (s *Service) run(ctx context.Context) { } func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error { - if s.getFlagValueFn(disableNotificationsKillSwitch) { + if s.getFlagValueFn(unleash.EventLoopNotificationDisabled) { s.log.Info("Received notification events. Skipping as kill switch is enabled.") return nil } diff --git a/internal/unleash/service.go b/internal/unleash/service.go index 174cd639..56f56135 100644 --- a/internal/unleash/service.go +++ b/internal/unleash/service.go @@ -36,6 +36,11 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals const filename = "unleash_flags" +const ( + EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" + IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" +) + type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error) type GetFlagValueFn func(key string) bool From 2e98d64f9491200663e6409bd616614102006ec0 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Fri, 22 Nov 2024 14:09:48 +0000 Subject: [PATCH 07/19] feat(BRIDGE-266): heartbeat telemetry update; extra integration tests; --- internal/bridge/heartbeat.go | 8 +- internal/bridge/identifier.go | 7 +- internal/bridge/user.go | 7 +- internal/plan/plan.go | 87 +++++++++ .../observability/distinction_utility.go | 3 +- internal/services/observability/heartbeat.go | 14 +- internal/services/observability/plan_utils.go | 85 +------- internal/services/observability/service.go | 4 +- internal/services/observability/test_utils.go | 21 +- internal/services/observability/utils_test.go | 5 - internal/telemetry/heartbeat.go | 89 ++++----- internal/telemetry/heartbeat_test.go | 64 ++++-- internal/telemetry/types_heartbeat.go | 62 ++++-- internal/user/user.go | 19 +- tests/features/bridge/heartbeat.feature | 184 +++++++++++++----- tests/heartbeat_test.go | 6 +- tests/steps_test.go | 2 + 17 files changed, 411 insertions(+), 256 deletions(-) create mode 100644 internal/plan/plan.go diff --git a/internal/bridge/heartbeat.go b/internal/bridge/heartbeat.go index 9abf1b66..8f2e46d2 100644 --- a/internal/bridge/heartbeat.go +++ b/internal/bridge/heartbeat.go @@ -73,15 +73,15 @@ func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager for _, user := range bridge.users { if user.GetAddressMode() == vault.SplitMode { splitMode = true - break } + h.SetUserPlan(user.GetUserPlanName()) } - var nbAccount = len(bridge.users) - h.SetNbAccount(nbAccount) + var numberConnectedAccounts = len(bridge.users) + h.SetNumberConnectedAccounts(numberConnectedAccounts) h.SetSplitMode(splitMode) // Do not try to send if there is no user yet. - if nbAccount > 0 { + if numberConnectedAccounts > 0 { defer h.start() } }, bridge.usersLock) diff --git a/internal/bridge/identifier.go b/internal/bridge/identifier.go index 3ba22b47..a707a5d8 100644 --- a/internal/bridge/identifier.go +++ b/internal/bridge/identifier.go @@ -17,7 +17,9 @@ package bridge -import "github.com/sirupsen/logrus" +import ( + "github.com/sirupsen/logrus" +) func (bridge *Bridge) GetCurrentUserAgent() string { return bridge.identifier.GetUserAgent() @@ -30,6 +32,8 @@ func (bridge *Bridge) SetCurrentPlatform(platform string) { func (bridge *Bridge) setUserAgent(name, version string) { currentUserAgent := bridge.identifier.GetClientString() + bridge.heartbeat.SetContactedByAppleNotes(name) + bridge.identifier.SetClient(name, version) newUserAgent := bridge.identifier.GetClientString() @@ -54,6 +58,7 @@ func (b *bridgeUserAgentUpdater) HasClient() bool { } func (b *bridgeUserAgentUpdater) SetClient(name, version string) { + b.heartbeat.SetContactedByAppleNotes(name) b.identifier.SetClient(name, version) } diff --git a/internal/bridge/user.go b/internal/bridge/user.go index efde55f4..717d6c29 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -583,9 +583,12 @@ func (bridge *Bridge) addUserWithVault( // Finally, save the user in the bridge. safe.Lock(func() { bridge.users[apiUser.ID] = user - bridge.heartbeat.SetNbAccount(len(bridge.users)) + bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users)) }, bridge.usersLock) + // Set user plan if its of a higher rank. + bridge.heartbeat.SetUserPlan(user.GetUserPlanName()) + // As we need at least one user to send heartbeat, try to send it. bridge.heartbeat.start() @@ -618,7 +621,7 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, logUser.WithError(err).Error("Failed to logout user") } - bridge.heartbeat.SetNbAccount(len(bridge.users)) + bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1) user.Close() } diff --git a/internal/plan/plan.go b/internal/plan/plan.go new file mode 100644 index 00000000..69248730 --- /dev/null +++ b/internal/plan/plan.go @@ -0,0 +1,87 @@ +// Copyright (c) 2024 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 . + +package plan + +import "strings" + +const ( + Unknown = "unknown" + Other = "other" + Business = "business" + Individual = "individual" + Group = "group" +) + +var planHierarchy = map[string]int{ //nolint:gochecknoglobals + Business: 4, + Group: 3, + Individual: 2, + Other: 1, + Unknown: 0, +} + +func IsHigherPriority(currentPlan, newPlan string) bool { + newRank, ok := planHierarchy[newPlan] + if !ok { + return false + } + + currentRank, ok2 := planHierarchy[currentPlan] + if !ok2 { + return true // we don't have a valid plan, might as well replace it + } + + return newRank > currentRank +} + +func MapUserPlan(planName string) string { + if planName == "" { + return Unknown + } + switch strings.TrimSpace(strings.ToLower(planName)) { + case Individual: + return Individual + case Unknown: + return Unknown + case Business: + return Business + case Group: + return Group + case "mail2022": + return Individual + case "bundle2022": + return Individual + case "family2022": + return Group + case "visionary2022": + return Group + case "mailpro2022": + return Business + case "planbiz2024": + return Business + case "bundlepro2022": + return Business + case "bundlepro2024": + return Business + case "duo2024": + return Group + + default: + return Other + } +} diff --git a/internal/services/observability/distinction_utility.go b/internal/services/observability/distinction_utility.go index 98534d60..9195130b 100644 --- a/internal/services/observability/distinction_utility.go +++ b/internal/services/observability/distinction_utility.go @@ -24,6 +24,7 @@ import ( "github.com/ProtonMail/gluon/async" "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" "github.com/ProtonMail/proton-bridge/v3/internal/updater" ) @@ -62,7 +63,7 @@ func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender: observabilitySender, - userPlanUnsafe: planUnknown, + userPlanUnsafe: plan.Unknown, heartbeatData: heartbeatData{}, heartbeatTicker: time.NewTicker(updateInterval), diff --git a/internal/services/observability/heartbeat.go b/internal/services/observability/heartbeat.go index 78f119d2..74eb930a 100644 --- a/internal/services/observability/heartbeat.go +++ b/internal/services/observability/heartbeat.go @@ -18,7 +18,7 @@ package observability import ( - "fmt" + "strconv" "time" "github.com/ProtonMail/gluon/async" @@ -87,10 +87,6 @@ func (d *distinctionUtility) sendHeartbeat() { }) } -func formatBool(value bool) string { - return fmt.Sprintf("%t", value) -} - // generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data. func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric { return generateHeartbeatMetric( @@ -98,10 +94,10 @@ func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityM d.getEmailClientUserAgent(), getEnabled(d.settingsGetter.GetProxyAllowed()), getEnabled(d.getBetaAccessEnabled()), - formatBool(d.heartbeatData.receivedOtherError), - formatBool(d.heartbeatData.receivedSyncError), - formatBool(d.heartbeatData.receivedEventLoopError), - formatBool(d.heartbeatData.receivedGluonError), + strconv.FormatBool(d.heartbeatData.receivedOtherError), + strconv.FormatBool(d.heartbeatData.receivedSyncError), + strconv.FormatBool(d.heartbeatData.receivedEventLoopError), + strconv.FormatBool(d.heartbeatData.receivedGluonError), ) } diff --git a/internal/services/observability/plan_utils.go b/internal/services/observability/plan_utils.go index 3952b792..e8e247ca 100644 --- a/internal/services/observability/plan_utils.go +++ b/internal/services/observability/plan_utils.go @@ -18,76 +18,9 @@ package observability import ( - "context" - "strings" - - "github.com/ProtonMail/gluon/async" - "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" ) -const ( - planUnknown = "unknown" - planOther = "other" - planBusiness = "business" - planIndividual = "individual" - planGroup = "group" -) - -var planHierarchy = map[string]int{ //nolint:gochecknoglobals - planBusiness: 4, - planGroup: 3, - planIndividual: 2, - planOther: 1, - planUnknown: 0, -} - -type planGetter interface { - GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error) -} - -func isHigherPriority(currentPlan, newPlan string) bool { - newRank, ok := planHierarchy[newPlan] - if !ok { - return false - } - - currentRank, ok2 := planHierarchy[currentPlan] - if !ok2 { - return true // we don't have a valid plan, might as well replace it - } - - return newRank > currentRank -} - -func mapUserPlan(planName string) string { - if planName == "" { - return planUnknown - } - switch strings.TrimSpace(strings.ToLower(planName)) { - case "mail2022": - return planIndividual - case "bundle2022": - return planIndividual - case "family2022": - return planGroup - case "visionary2022": - return planGroup - case "mailpro2022": - return planBusiness - case "planbiz2024": - return planBusiness - case "bundlepro2022": - return planBusiness - case "bundlepro2024": - return planBusiness - case "duo2024": - return planGroup - - default: - return planOther - } -} - func (d *distinctionUtility) setUserPlan(planName string) { if planName == "" { return @@ -96,24 +29,12 @@ func (d *distinctionUtility) setUserPlan(planName string) { d.userPlanLock.Lock() defer d.userPlanLock.Unlock() - userPlanMapped := mapUserPlan(planName) - if isHigherPriority(d.userPlanUnsafe, userPlanMapped) { + userPlanMapped := plan.MapUserPlan(planName) + if plan.IsHigherPriority(d.userPlanUnsafe, userPlanMapped) { d.userPlanUnsafe = userPlanMapped } } -func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) { - go func() { - defer async.HandlePanic(panicHandler) - - orgRes, err := getter.GetOrganizationData(ctx) - if err != nil { - return - } - d.setUserPlan(orgRes.Organization.PlanName) - }() -} - func (d *distinctionUtility) getUserPlanSafe() string { d.userPlanLock.Lock() defer d.userPlanLock.Unlock() diff --git a/internal/services/observability/service.go b/internal/services/observability/service.go index 86b4747a..58e1428d 100644 --- a/internal/services/observability/service.go +++ b/internal/services/observability/service.go @@ -250,7 +250,7 @@ func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) { s.addMetrics(metric...) } -func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) { +func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service, userPlan string) { s.log.Info("Registering user client, ID:", userID) s.withUserClientStoreLock(func() { @@ -260,7 +260,7 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, } }) - s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler) + s.distinctionUtility.setUserPlan(userPlan) // There may be a case where we already have metric updates stored, so try to flush; s.sendSignal(s.signalDataArrived) diff --git a/internal/services/observability/test_utils.go b/internal/services/observability/test_utils.go index 115b28be..b8a80edb 100644 --- a/internal/services/observability/test_utils.go +++ b/internal/services/observability/test_utils.go @@ -20,15 +20,16 @@ package observability import ( gluonMetrics "github.com/ProtonMail/gluon/observability/metrics" "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" ) func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric { planValues := []string{ - planUnknown, - planOther, - planBusiness, - planIndividual, - planGroup} + plan.Unknown, + plan.Other, + plan.Business, + plan.Individual, + plan.Group} mailClientValues := []string{ emailAgentAppleMail, emailAgentOutlook, @@ -58,11 +59,11 @@ func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric { planValues := []string{ - planUnknown, - planOther, - planBusiness, - planIndividual, - planGroup} + plan.Unknown, + plan.Other, + plan.Business, + plan.Individual, + plan.Group} mailClientValues := []string{ emailAgentAppleMail, emailAgentOutlook, diff --git a/internal/services/observability/utils_test.go b/internal/services/observability/utils_test.go index 5001cf1f..095dc2bb 100644 --- a/internal/services/observability/utils_test.go +++ b/internal/services/observability/utils_test.go @@ -104,8 +104,3 @@ func TestMatchUserAgent(t *testing.T) { require.Equal(t, testCase.result, matchUserAgent(testCase.agent)) } } - -func TestFormatBool(t *testing.T) { - require.Equal(t, "false", formatBool(false)) - require.Equal(t, "true", formatBool(true)) -} diff --git a/internal/telemetry/heartbeat.go b/internal/telemetry/heartbeat.go index 0c9649c1..f682af03 100644 --- a/internal/telemetry/heartbeat.go +++ b/internal/telemetry/heartbeat.go @@ -19,9 +19,12 @@ package telemetry import ( "context" + "math" "strconv" + "strings" "time" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" "github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/sirupsen/logrus" ) @@ -32,70 +35,66 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke manager: manager, metrics: HeartbeatData{ MeasurementGroup: "bridge.any.usage", - Event: "bridge_heartbeat", + Event: "bridge_heartbeat_new", + Dimensions: NewHeartbeatDimensions(), }, defaultIMAPPort: imapPort, defaultSMTPPort: smtpPort, defaultCache: cacheDir, defaultKeychain: keychain, + defaultUserPlan: plan.Unknown, } return heartbeat } func (heartbeat *Heartbeat) SetRollout(val float64) { - heartbeat.metrics.Dimensions.Rollout = strconv.Itoa(int(val * 100)) + heartbeat.metrics.Values.Rollout = int(math.Floor(val * 10)) } -func (heartbeat *Heartbeat) SetNbAccount(val int) { - heartbeat.metrics.Values.NbAccount = val +func (heartbeat *Heartbeat) GetRollout() int { + return heartbeat.metrics.Values.Rollout +} + +func (heartbeat *Heartbeat) SetNumberConnectedAccounts(val int) { + heartbeat.metrics.Values.NumberConnectedAccounts = val } func (heartbeat *Heartbeat) SetAutoUpdate(val bool) { - if val { - heartbeat.metrics.Dimensions.AutoUpdate = dimensionON - } else { - heartbeat.metrics.Dimensions.AutoUpdate = dimensionOFF - } + heartbeat.metrics.Dimensions.AutoUpdateEnabled = strconv.FormatBool(val) } func (heartbeat *Heartbeat) SetAutoStart(val bool) { - if val { - heartbeat.metrics.Dimensions.AutoStart = dimensionON - } else { - heartbeat.metrics.Dimensions.AutoStart = dimensionOFF - } + heartbeat.metrics.Dimensions.AutoStartEnabled = strconv.FormatBool(val) } func (heartbeat *Heartbeat) SetBeta(val updater.Channel) { - if val == updater.EarlyChannel { - heartbeat.metrics.Dimensions.Beta = dimensionON - } else { - heartbeat.metrics.Dimensions.Beta = dimensionOFF - } + heartbeat.metrics.Dimensions.BetaEnabled = strconv.FormatBool(val == updater.EarlyChannel) } func (heartbeat *Heartbeat) SetDoh(val bool) { - if val { - heartbeat.metrics.Dimensions.Doh = dimensionON - } else { - heartbeat.metrics.Dimensions.Doh = dimensionOFF - } + heartbeat.metrics.Dimensions.DohEnabled = strconv.FormatBool(val) } func (heartbeat *Heartbeat) SetSplitMode(val bool) { - if val { - heartbeat.metrics.Dimensions.SplitMode = dimensionON - } else { - heartbeat.metrics.Dimensions.SplitMode = dimensionOFF + heartbeat.metrics.Dimensions.UseSplitMode = strconv.FormatBool(val) +} + +func (heartbeat *Heartbeat) SetUserPlan(val string) { + mappedUserPlan := plan.MapUserPlan(val) + if plan.IsHigherPriority(heartbeat.metrics.Dimensions.UserPlanGroup, mappedUserPlan) { + heartbeat.metrics.Dimensions.UserPlanGroup = val + } +} + +func (heartbeat *Heartbeat) SetContactedByAppleNotes(uaName string) { + uaNameLowered := strings.ToLower(uaName) + if strings.Contains(uaNameLowered, "mac") && strings.Contains(uaNameLowered, "notes") { + heartbeat.metrics.Dimensions.ContactedByAppleNotes = strconv.FormatBool(true) } } func (heartbeat *Heartbeat) SetShowAllMail(val bool) { - if val { - heartbeat.metrics.Dimensions.ShowAllMail = dimensionON - } else { - heartbeat.metrics.Dimensions.ShowAllMail = dimensionOFF - } + heartbeat.metrics.Dimensions.ShowAllMail = strconv.FormatBool(val) } func (heartbeat *Heartbeat) SetIMAPConnectionMode(val bool) { @@ -115,35 +114,19 @@ func (heartbeat *Heartbeat) SetSMTPConnectionMode(val bool) { } func (heartbeat *Heartbeat) SetIMAPPort(val int) { - if val == heartbeat.defaultIMAPPort { - heartbeat.metrics.Dimensions.IMAPPort = dimensionDefault - } else { - heartbeat.metrics.Dimensions.IMAPPort = dimensionCustom - } + heartbeat.metrics.Dimensions.UseDefaultIMAPPort = strconv.FormatBool(val == heartbeat.defaultIMAPPort) } func (heartbeat *Heartbeat) SetSMTPPort(val int) { - if val == heartbeat.defaultSMTPPort { - heartbeat.metrics.Dimensions.SMTPPort = dimensionDefault - } else { - heartbeat.metrics.Dimensions.SMTPPort = dimensionCustom - } + heartbeat.metrics.Dimensions.UseDefaultSMTPPort = strconv.FormatBool(val == heartbeat.defaultSMTPPort) } func (heartbeat *Heartbeat) SetCacheLocation(val string) { - if val == heartbeat.defaultCache { - heartbeat.metrics.Dimensions.CacheLocation = dimensionDefault - } else { - heartbeat.metrics.Dimensions.CacheLocation = dimensionCustom - } + heartbeat.metrics.Dimensions.UseDefaultCacheLocation = strconv.FormatBool(val == heartbeat.defaultCache) } func (heartbeat *Heartbeat) SetKeyChainPref(val string) { - if val == heartbeat.defaultKeychain { - heartbeat.metrics.Dimensions.KeychainPref = dimensionDefault - } else { - heartbeat.metrics.Dimensions.KeychainPref = dimensionCustom - } + heartbeat.metrics.Dimensions.UseDefaultKeychain = strconv.FormatBool(val == heartbeat.defaultKeychain) } func (heartbeat *Heartbeat) SetPrevVersion(val string) { diff --git a/internal/telemetry/heartbeat_test.go b/internal/telemetry/heartbeat_test.go index 1a9dd052..101282ad 100644 --- a/internal/telemetry/heartbeat_test.go +++ b/internal/telemetry/heartbeat_test.go @@ -22,34 +22,38 @@ import ( "testing" "time" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" ) func TestHeartbeat_default_heartbeat(t *testing.T) { withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) { data := telemetry.HeartbeatData{ MeasurementGroup: "bridge.any.usage", - Event: "bridge_heartbeat", + Event: "bridge_heartbeat_new", Values: telemetry.HeartbeatValues{ - NbAccount: 1, + NumberConnectedAccounts: 1, + Rollout: 1, }, Dimensions: telemetry.HeartbeatDimensions{ - AutoUpdate: "on", - AutoStart: "on", - Beta: "off", - Doh: "off", - SplitMode: "off", - ShowAllMail: "off", - IMAPConnectionMode: "ssl", - SMTPConnectionMode: "ssl", - IMAPPort: "default", - SMTPPort: "default", - CacheLocation: "default", - KeychainPref: "default", - PrevVersion: "1.2.3", - Rollout: "10", + AutoUpdateEnabled: "true", + AutoStartEnabled: "true", + BetaEnabled: "false", + DohEnabled: "false", + UseSplitMode: "false", + ShowAllMail: "false", + UseDefaultIMAPPort: "true", + UseDefaultSMTPPort: "true", + UseDefaultCacheLocation: "true", + UseDefaultKeychain: "true", + ContactedByAppleNotes: "false", + PrevVersion: "1.2.3", + IMAPConnectionMode: "ssl", + SMTPConnectionMode: "ssl", + UserPlanGroup: plan.Unknown, }, } @@ -81,7 +85,7 @@ func withHeartbeat(t *testing.T, imap, smtp int, cache, keychain string, tests f heartbeat := telemetry.NewHeartbeat(manager, imap, smtp, cache, keychain) heartbeat.SetRollout(0.1) - heartbeat.SetNbAccount(1) + heartbeat.SetNumberConnectedAccounts(1) heartbeat.SetSplitMode(false) heartbeat.SetAutoStart(true) heartbeat.SetAutoUpdate(true) @@ -98,3 +102,29 @@ func withHeartbeat(t *testing.T, imap, smtp int, cache, keychain string, tests f tests(&heartbeat, manager) } + +func Test_setRollout(t *testing.T) { + hb := telemetry.Heartbeat{} + type testStruct struct { + val float64 + res int + } + + tests := []testStruct{ + {0.02, 0}, + {0.04, 0}, + {0.09999, 0}, + {0.1, 1}, + {0.132323, 1}, + {0.2, 2}, + {0.25, 2}, + {0.7111, 7}, + {0.93, 9}, + {0.999, 9}, + } + + for _, test := range tests { + hb.SetRollout(test.val) + require.Equal(t, test.res, hb.GetRollout()) + } +} diff --git a/internal/telemetry/types_heartbeat.go b/internal/telemetry/types_heartbeat.go index 3df472c6..34d9d6db 100644 --- a/internal/telemetry/types_heartbeat.go +++ b/internal/telemetry/types_heartbeat.go @@ -21,14 +21,11 @@ import ( "context" "time" + "github.com/ProtonMail/proton-bridge/v3/internal/plan" "github.com/sirupsen/logrus" ) const ( - dimensionON = "on" - dimensionOFF = "off" - dimensionDefault = "default" - dimensionCustom = "custom" dimensionSSL = "ssl" dimensionStartTLS = "starttls" ) @@ -46,24 +43,29 @@ type HeartbeatManager interface { } type HeartbeatValues struct { - NbAccount int `json:"nb_account"` + NumberConnectedAccounts int `json:"numberConnectedAccounts"` + Rollout int `json:"rolloutPercentage"` } type HeartbeatDimensions struct { - AutoUpdate string `json:"auto_update"` - AutoStart string `json:"auto_start"` - Beta string `json:"beta"` - Doh string `json:"doh"` - SplitMode string `json:"split_mode"` - ShowAllMail string `json:"show_all_mail"` - IMAPConnectionMode string `json:"imap_connection_mode"` - SMTPConnectionMode string `json:"smtp_connection_mode"` - IMAPPort string `json:"imap_port"` - SMTPPort string `json:"smtp_port"` - CacheLocation string `json:"cache_location"` - KeychainPref string `json:"keychain_pref"` - PrevVersion string `json:"prev_version"` - Rollout string `json:"rollout"` + // Fields below correspond to bool + AutoUpdateEnabled string `json:"isAutoUpdateEnabled"` + AutoStartEnabled string `json:"isAutoStartEnabled"` + BetaEnabled string `json:"isBetaEnabled"` + DohEnabled string `json:"isDohEnabled"` + UseSplitMode string `json:"usesSplitMode"` + ShowAllMail string `json:"useAllMail"` + UseDefaultIMAPPort string `json:"useDefaultImapPort"` + UseDefaultSMTPPort string `json:"useDefaultSmtpPort"` + UseDefaultCacheLocation string `json:"useDefaultCacheLocation"` + UseDefaultKeychain string `json:"useDefaultKeychain"` + ContactedByAppleNotes string `json:"isContactedByAppleNotes"` + + // Fields below are enums. + PrevVersion string `json:"prevVersion"` // Free text (exception) + IMAPConnectionMode string `json:"imapConnectionMode"` + SMTPConnectionMode string `json:"smtpConnectionMode"` + UserPlanGroup string `json:"bridgePlanGroup"` } type HeartbeatData struct { @@ -82,4 +84,26 @@ type Heartbeat struct { defaultSMTPPort int defaultCache string defaultKeychain string + defaultUserPlan string +} + +func NewHeartbeatDimensions() HeartbeatDimensions { + return HeartbeatDimensions{ + AutoUpdateEnabled: "false", + AutoStartEnabled: "false", + BetaEnabled: "false", + DohEnabled: "false", + UseSplitMode: "false", + ShowAllMail: "false", + UseDefaultIMAPPort: "false", + UseDefaultSMTPPort: "false", + UseDefaultCacheLocation: "false", + UseDefaultKeychain: "false", + ContactedByAppleNotes: "false", + + PrevVersion: "unknown", + IMAPConnectionMode: dimensionSSL, + SMTPConnectionMode: dimensionSSL, + UserPlanGroup: plan.Unknown, + } } diff --git a/internal/user/user.go b/internal/user/user.go index b9c1170c..0626588d 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -63,6 +63,8 @@ type User struct { id string log *logrus.Entry + userPlan string + vault *vault.User client *proton.Client reporter reporter.Reporter @@ -176,6 +178,14 @@ func newImpl( return nil, fmt.Errorf("failed to get addresses: %w", err) } + // Get the user's plan name. + var userPlan string + if organizationData, err := client.GetOrganizationData(ctx); err != nil { + logrus.WithError(err).Info("Failed to obtain user organization data") + } else { + userPlan = organizationData.Organization.Name + } + // Get the user's API labels. apiLabels, err := client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel) if err != nil { @@ -197,6 +207,8 @@ func newImpl( log: logrus.WithField("userID", apiUser.ID), id: apiUser.ID, + userPlan: userPlan, + vault: encVault, client: client, reporter: reporter, @@ -317,7 +329,7 @@ func newImpl( user.identityService.Start(ctx, user.serviceGroup) // Add user client to observability service - observabilityService.RegisterUserClient(user.id, client, user.telemetryService) + observabilityService.RegisterUserClient(user.id, client, user.telemetryService, userPlan) // Start Notification service user.notificationService.Start(ctx, user.serviceGroup) @@ -416,6 +428,11 @@ func (user *User) GetAddressMode() vault.AddressMode { return user.vault.AddressMode() } +// GetUserPlanName returns the user's subscription plan name. +func (user *User) GetUserPlanName() string { + return user.userPlan +} + // SetAddressMode sets the user's address mode. func (user *User) SetAddressMode(ctx context.Context, mode vault.AddressMode) error { user.log.WithField("mode", mode).Info("Setting address mode") diff --git a/tests/features/bridge/heartbeat.feature b/tests/features/bridge/heartbeat.feature index 1288b7c5..25aa5b40 100644 --- a/tests/features/bridge/heartbeat.feature +++ b/tests/features/bridge/heartbeat.feature @@ -1,6 +1,8 @@ Feature: Send Telemetry Heartbeat Background: Given there exists an account with username "[user:user1]" and password "password" + And there exists an account with username "[user:user2]" and password "password" + And there exists an account with username "[user:user3]" and password "password" Then it succeeds When bridge starts Then it succeeds @@ -12,28 +14,30 @@ Feature: Send Telemetry Heartbeat When the user logs in with username "[user:user1]" and password "password" And user "[user:user1]" finishes syncing Then bridge eventually sends the following heartbeat: - """ + """ { "MeasurementGroup": "bridge.any.usage", - "Event": "bridge_heartbeat", + "Event": "bridge_heartbeat_new", "Values": { - "nb_account": 1 + "NumberConnectedAccounts": 1, + "rolloutPercentage": 1 }, "Dimensions": { - "auto_update": "on", - "auto_start": "on", - "beta": "off", - "doh": "off", - "split_mode": "off", - "show_all_mail": "on", - "imap_connection_mode": "starttls", - "smtp_connection_mode": "starttls", - "imap_port": "default", - "smtp_port": "default", - "cache_location": "default", - "keychain_pref": "default", - "prev_version": "0.0.0", - "rollout": "42" + "isAutoUpdateEnabled": "true", + "isAutoStartEnabled": "true", + "isBetaEnabled": "false", + "isDohEnabled": "false", + "usesSplitMode": "false", + "useAllMail": "true", + "useDefaultImapPort": "true", + "useDefaultSmtpPort": "true", + "useDefaultCacheLocation": "true", + "useDefaultKeychain": "true", + "isContactedByAppleNotes": "false", + "imapConnectionMode": "starttls", + "smtpConnectionMode": "starttls", + "prevVersion": "0.0.0", + "bridgePlanGroup": "unknown" } } """ @@ -59,25 +63,27 @@ Feature: Send Telemetry Heartbeat """ { "MeasurementGroup": "bridge.any.usage", - "Event": "bridge_heartbeat", + "Event": "bridge_heartbeat_new", "Values": { - "nb_account": 1 + "NumberConnectedAccounts": 1, + "rolloutPercentage": 1 }, "Dimensions": { - "auto_update": "off", - "auto_start": "off", - "beta": "off", - "doh": "on", - "split_mode": "off", - "show_all_mail": "off", - "imap_connection_mode": "ssl", - "smtp_connection_mode": "ssl", - "imap_port": "custom", - "smtp_port": "custom", - "cache_location": "custom", - "keychain_pref": "custom", - "prev_version": "0.0.0", - "rollout": "42" + "isAutoUpdateEnabled": "false", + "isAutoStartEnabled": "false", + "isBetaEnabled": "false", + "isDohEnabled": "true", + "usesSplitMode": "false", + "useAllMail": "false", + "useDefaultImapPort": "false", + "useDefaultSmtpPort": "false", + "useDefaultCacheLocation": "false", + "useDefaultKeychain": "false", + "isContactedByAppleNotes": "false", + "imapConnectionMode": "ssl", + "smtpConnectionMode": "ssl", + "prevVersion": "0.0.0", + "bridgePlanGroup": "unknown" } } """ @@ -97,25 +103,105 @@ Feature: Send Telemetry Heartbeat """ { "MeasurementGroup": "bridge.any.usage", - "Event": "bridge_heartbeat", + "Event": "bridge_heartbeat_new", "Values": { - "nb_account": 1 + "NumberConnectedAccounts": 1, + "rolloutPercentage": 1 }, "Dimensions": { - "auto_update": "on", - "auto_start": "on", - "beta": "off", - "doh": "off", - "split_mode": "on", - "show_all_mail": "on", - "imap_connection_mode": "starttls", - "smtp_connection_mode": "starttls", - "imap_port": "default", - "smtp_port": "default", - "cache_location": "default", - "keychain_pref": "default", - "prev_version": "0.0.0", - "rollout": "42" + "isAutoUpdateEnabled": "true", + "isAutoStartEnabled": "true", + "isBetaEnabled": "false", + "isDohEnabled": "false", + "usesSplitMode": "true", + "useAllMail": "true", + "useDefaultImapPort": "true", + "useDefaultSmtpPort": "true", + "useDefaultCacheLocation": "true", + "useDefaultKeychain": "true", + "isContactedByAppleNotes": "false", + "imapConnectionMode": "starttls", + "smtpConnectionMode": "starttls", + "prevVersion": "0.0.0", + "bridgePlanGroup": "unknown" + } + } + """ + And bridge do not need to send heartbeat + + + Scenario: Multiple-users on Bridge reported correctly + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user1]" and password "password" + Then it succeeds + When the user logs in with username "[user:user2]" and password "password" + Then it succeeds + When the user logs in with username "[user:user3]" and password "password" + Then it succeeds + When bridge needs to explicitly send heartbeat + Then bridge eventually sends the following heartbeat: + """ + { + "MeasurementGroup": "bridge.any.usage", + "Event": "bridge_heartbeat_new", + "Values": { + "NumberConnectedAccounts": 3, + "rolloutPercentage": 1 + }, + "Dimensions": { + "isAutoUpdateEnabled": "true", + "isAutoStartEnabled": "true", + "isBetaEnabled": "false", + "isDohEnabled": "false", + "usesSplitMode": "false", + "useAllMail": "true", + "useDefaultImapPort": "true", + "useDefaultSmtpPort": "true", + "useDefaultCacheLocation": "true", + "useDefaultKeychain": "true", + "isContactedByAppleNotes": "false", + "imapConnectionMode": "starttls", + "smtpConnectionMode": "starttls", + "prevVersion": "0.0.0", + "bridgePlanGroup": "unknown" + } + } + """ + And bridge do not need to send heartbeat + + + Scenario: Send heartbeat explicitly - apple notes tried to connect + Then bridge telemetry feature is enabled + When the user logs in with username "[user:user1]" and password "password" + Then it succeeds + When user "[user:user1]" connects IMAP client "1" + And IMAP client "1" announces its ID with name "Mac OS X Notes" and version "14.5" + When bridge needs to explicitly send heartbeat + Then bridge eventually sends the following heartbeat: + """ + { + "MeasurementGroup": "bridge.any.usage", + "Event": "bridge_heartbeat_new", + "Values": { + "NumberConnectedAccounts": 1, + "rolloutPercentage": 1 + }, + "Dimensions": { + "isAutoUpdateEnabled": "true", + "isAutoStartEnabled": "true", + "isBetaEnabled": "false", + "isDohEnabled": "false", + "usesSplitMode": "false", + "useAllMail": "true", + "useDefaultImapPort": "true", + "useDefaultSmtpPort": "true", + "useDefaultCacheLocation": "true", + "useDefaultKeychain": "true", + "isContactedByAppleNotes": "true", + "imapConnectionMode": "starttls", + "smtpConnectionMode": "starttls", + "prevVersion": "0.0.0", + "bridgePlanGroup": "unknown" } } """ diff --git a/tests/heartbeat_test.go b/tests/heartbeat_test.go index 086c72ad..cde05af5 100644 --- a/tests/heartbeat_test.go +++ b/tests/heartbeat_test.go @@ -54,6 +54,10 @@ func (s *scenario) bridgeNeedsToSendHeartbeat() error { return nil } +func (s *scenario) bridgeNeedsToSendExplicitHeartbeat() error { + return s.t.heartbeat.SetLastHeartbeatSent(time.Now().Add(-24 * time.Hour)) +} + func (s *scenario) bridgeDoNotNeedToSendHeartbeat() error { last := s.t.heartbeat.GetLastHeartbeatSent() if isAnotherDay(last, time.Now()) { @@ -73,7 +77,7 @@ func matchHeartbeat(have, want telemetry.HeartbeatData) error { } // Ignore rollout number - want.Dimensions.Rollout = have.Dimensions.Rollout + want.Values.Rollout = have.Values.Rollout if have != want { return fmt.Errorf("missing heartbeat: have %#v, want %#v", have, want) diff --git a/tests/steps_test.go b/tests/steps_test.go index 12c8cfff..b3f73be0 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -207,6 +207,8 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { // ==== TELEMETRY ==== ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat) ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat) + ctx.Step(`^bridge needs to explicitly send heartbeat`, s.bridgeNeedsToSendExplicitHeartbeat) + ctx.Step(`^bridge do not need to send heartbeat`, s.bridgeDoNotNeedToSendHeartbeat) ctx.Step(`^heartbeat is not whitelisted`, s.heartbeatIsNotwhitelisted) From af01c63298642d1b5d616df141dc5ecdee458da5 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Mon, 11 Nov 2024 15:49:23 +0100 Subject: [PATCH 08/19] fix(BRIDGE-261): delete gluon data during user deletion; integration tests; FF kill switch; Sentry report if error; --- internal/bridge/bridge.go | 5 ++ internal/bridge/user.go | 5 +- internal/services/imapservice/service.go | 13 +++++ internal/unleash/service.go | 5 +- internal/user/user.go | 25 +++++++-- tests/features/user/delete_imap.feature | 25 +++++++++ tests/steps_test.go | 1 + tests/user_test.go | 66 ++++++++++++++++++++++++ 8 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 tests/features/user/delete_imap.feature diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 88ef7676..fc9a3b00 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -733,3 +733,8 @@ func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx report }).Info("Error occurred when sending Report to Sentry") } } + +// GetUsers is only used for testing purposes. +func (bridge *Bridge) GetUsers() map[string]*user.User { + return bridge.users +} diff --git a/internal/bridge/user.go b/internal/bridge/user.go index 717d6c29..fda3e1b3 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -33,6 +33,7 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" "github.com/ProtonMail/proton-bridge/v3/internal/try" + "github.com/ProtonMail/proton-bridge/v3/internal/unleash" "github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/go-resty/resty/v2" @@ -607,7 +608,7 @@ func (bridge *Bridge) newVaultUser( return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass) } -// logout logs out the given user, optionally logging them out from the API too. +// logoutUser logs out the given user, optionally logging them out from the API and deleting user related gluon data. func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) { defer delete(bridge.users, user.ID()) @@ -617,7 +618,7 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, "withData": withData, }).Debug("Logging out user") - if err := user.Logout(ctx, withAPI); err != nil { + if err := user.Logout(ctx, withAPI, withData, bridge.unleashService.GetFlagValue(unleash.UserRemovalGluonDataCleanupDisabled)); err != nil { logUser.WithError(err).Error("Failed to logout user") } diff --git a/internal/services/imapservice/service.go b/internal/services/imapservice/service.go index 989b78fb..99d18788 100644 --- a/internal/services/imapservice/service.go +++ b/internal/services/imapservice/service.go @@ -233,6 +233,12 @@ func (s *Service) OnLogout(ctx context.Context) error { return err } +func (s *Service) OnDelete(ctx context.Context) error { + _, err := s.cpc.Send(ctx, &onDeleteReq{}) + + return err +} + func (s *Service) ShowAllMail(ctx context.Context, v bool) error { _, err := s.cpc.Send(ctx, &showAllMailReq{v: v}) @@ -362,6 +368,11 @@ func (s *Service) run(ctx context.Context) { //nolint gocyclo err := s.removeConnectorsFromServer(ctx, s.connectors, false) req.Reply(ctx, nil, err) + case *onDeleteReq: + s.log.Debug("Delete Request") + err := s.removeConnectorsFromServer(ctx, s.connectors, true) + req.Reply(ctx, nil, err) + case *showAllMailReq: s.log.Debug("Show all mail request") req.Reply(ctx, nil, nil) @@ -644,6 +655,8 @@ type onLogoutReq struct{} type showAllMailReq struct{ v bool } +type onDeleteReq struct{} + type setAddressModeReq struct { mode usertypes.AddressMode } diff --git a/internal/unleash/service.go b/internal/unleash/service.go index 56f56135..0ae4fb48 100644 --- a/internal/unleash/service.go +++ b/internal/unleash/service.go @@ -37,8 +37,9 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals const filename = "unleash_flags" const ( - EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" - IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" + EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" + IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" + UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" ) type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error) diff --git a/internal/user/user.go b/internal/user/user.go index 0626588d..536c1b3a 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -592,8 +592,13 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) { } // Logout logs the user out from the API. -func (user *User) Logout(ctx context.Context, withAPI bool) error { - user.log.WithField("withAPI", withAPI).Info("Logging out user") +func (user *User) Logout(ctx context.Context, withAPI, withData, withDataDisabledKillSwitch bool) error { + user.log.WithFields( + logrus.Fields{ + "withAPI": withAPI, + "withData": withData, + "withDataDisabledKillSwitch": withDataDisabledKillSwitch, + }).Info("Logging out user") user.log.Debug("Canceling ongoing tasks") @@ -601,8 +606,20 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error { return fmt.Errorf("failed to remove user from smtp server: %w", err) } - if err := user.imapService.OnLogout(ctx); err != nil { - return fmt.Errorf("failed to remove user from imap server: %w", err) + if withData && !withDataDisabledKillSwitch { + if err := user.imapService.OnDelete(ctx); err != nil { + if rerr := user.reporter.ReportMessageWithContext("Failed to delete user IMAP data", map[string]any{ + "error": err.Error(), + }); rerr != nil { + logrus.WithError(rerr).Info("Failed to report user IMAP deletion issue to Sentry") + } + + return fmt.Errorf("failed to delete user from imap server: %w", err) + } + } else { + if err := user.imapService.OnLogout(ctx); err != nil { + return fmt.Errorf("failed to remove user from imap server: %w", err) + } } user.tasks.CancelAndWait() diff --git a/tests/features/user/delete_imap.feature b/tests/features/user/delete_imap.feature new file mode 100644 index 00000000..0ffaa875 --- /dev/null +++ b/tests/features/user/delete_imap.feature @@ -0,0 +1,25 @@ +Feature: User deletion with IMAP data removal + Background: + Given there exists an account with username "[user:user]" and password "password" + And the account "[user:user]" has the following custom mailboxes: + | name | type | + | one | folder | + And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Folders/one": + | from | to | subject | unread | + | a@example.com | a@example.com | one | true | + | b@example.com | b@example.com | two | false | + | c@example.com | c@example.com | three | true | + | c@example.com | c@example.com | four | false | + Then it succeeds + When bridge starts + And the user logs in with username "[user:user]" and password "password" + And user "[user:user]" finishes syncing + Then it succeeds + + Scenario: User is deleted from Bridge and IMAP data is removed + When user "[user:user]" connects and authenticates IMAP client "1" + Then IMAP client "1" sees the following mailbox info for "Folders/one": + | name | total | unread | + | Folders/one | 4 | 2 | + And user "[user:user]" is deleted alongside IMAP data for client "1" + Then it succeeds diff --git a/tests/steps_test.go b/tests/steps_test.go index b3f73be0..7dd4085e 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -116,6 +116,7 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo) ctx.Step(`^the bridge password of user "([^"]*)" is changed to "([^"]*)"`, s.bridgePasswordOfUserIsChangedTo) ctx.Step(`^the bridge password of user "([^"]*)" is equal to "([^"]*)"`, s.bridgePasswordOfUserIsEqualTo) + ctx.Step(`^user "([^"]*)" is deleted alongside IMAP data for client "([^"]*)"$`, s.userIsDeletedAndImapDataRemoved) // ==== ACCOUNT SETTINGS ==== ctx.Step(`^the account "([^"]*)" has public key attachment "([^"]*)"`, s.accountHasPublicKeyAttachment) diff --git a/tests/user_test.go b/tests/user_test.go index 5ac4be9f..e7b56674 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -22,6 +22,8 @@ import ( "errors" "fmt" "net/mail" + "os" + "path/filepath" "strings" "time" @@ -388,6 +390,70 @@ func (s *scenario) userIsDeleted(username string) error { return s.t.bridge.DeleteUser(context.Background(), s.t.getUserByName(username).getUserID()) } +func (s *scenario) userIsDeletedAndImapDataRemoved(username string) error { + gluonCacheDir := s.t.bridge.GetGluonCacheDir() + userID := s.t.getUserByName(username).userID + userMap := s.t.bridge.GetUsers() + userObj, ok := userMap[userID] + if !ok { + return fmt.Errorf("could not find user object") + } + + gluonIDMap := userObj.GetGluonIDs() + gluonIDs := make([]string, 0, len(gluonIDMap)) + for _, id := range gluonIDMap { + gluonIDs = append(gluonIDs, id) + } + + var relevantPaths []string + if err := filepath.Walk(gluonCacheDir, func(path string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + for _, gluonID := range gluonIDs { + if strings.Contains(path, gluonID) { + relevantPaths = append(relevantPaths, path) + } + } + return nil + }); err != nil { + return err + } + + if len(relevantPaths) == 0 { + return fmt.Errorf("found no user related gluon paths") + } + + if err := s.t.bridge.DeleteUser(context.Background(), userID); err != nil { + return fmt.Errorf("could not delete user: %w", err) + } + + foundDeferredDelete := false + var remainingPaths []string + if err := filepath.Walk(gluonCacheDir, func(path string, _ os.FileInfo, err error) error { + if err != nil { + return err + } + for _, gluonID := range gluonIDs { + if strings.Contains(path, gluonID) { + remainingPaths = append(remainingPaths, path) + } + } + if strings.Contains(path, "deferred_delete") { + foundDeferredDelete = true + } + return nil + }); err != nil { + return err + } + + if len(remainingPaths) == 0 && foundDeferredDelete { + return nil + } + + return fmt.Errorf("user gluon data is still present or could not find deferred deletion directory") +} + func (s *scenario) theAuthOfUserIsRevoked(username string) error { return s.t.withClient(context.Background(), username, func(ctx context.Context, client *proton.Client) error { return client.AuthRevokeAll(ctx) From acf2fc32c418d7be6d9153599f97d2b9f67dc751 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Tue, 12 Nov 2024 17:23:08 +0100 Subject: [PATCH 09/19] fix(BRIDGE-264): ignore apple notes as User-Agentt --- internal/useragent/useragent.go | 6 +++++- internal/useragent/useragent_test.go | 8 ++++++++ tests/features/imap/id.feature | 27 ++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/internal/useragent/useragent.go b/internal/useragent/useragent.go index 23d21074..73da03a3 100644 --- a/internal/useragent/useragent.go +++ b/internal/useragent/useragent.go @@ -21,6 +21,7 @@ import ( "fmt" "regexp" "runtime" + "strings" "sync" ) @@ -42,9 +43,12 @@ func New() *UserAgent { } func (ua *UserAgent) SetClient(name, version string) { + if strings.EqualFold("Mac OS X Notes", name) { + return + } + ua.lock.Lock() defer ua.lock.Unlock() - ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2")) } diff --git a/internal/useragent/useragent_test.go b/internal/useragent/useragent_test.go index cb915a33..88b102a7 100644 --- a/internal/useragent/useragent_test.go +++ b/internal/useragent/useragent_test.go @@ -64,6 +64,14 @@ func TestUserAgent(t *testing.T) { platform: "Windows 10 (10.0)", want: "Thunderbird/78.6.1 (Windows 10 (10.0))", }, + + // We ignore Apple Notes. + { + name: "Mac OS X Notes", + version: "4.11", + platform: "Windows 10 (10.0)", + want: DefaultUserAgent + " (Windows 10 (10.0))", + }, } for _, test := range tests { diff --git a/tests/features/imap/id.feature b/tests/features/imap/id.feature index 9e3db21f..69259477 100644 --- a/tests/features/imap/id.feature +++ b/tests/features/imap/id.feature @@ -22,4 +22,29 @@ Feature: The IMAP ID is propagated to bridge When user "[user:user]" connects IMAP client "1" And IMAP client "1" announces its ID with name "name" and version "version" When the user reports a bug - Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "name/version ([GOOS])" \ No newline at end of file + Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "name/version ([GOOS])" + + Scenario: User agent re-announces a new ID to IMAP client + When user "[user:user]" connects IMAP client "1" + And IMAP client "1" announces its ID with name "name" and version "version" + Then the user agent is "name/version ([GOOS])" + And IMAP client "1" announces its ID with name "new_name" and version "new_version" + Then the user agent is "new_name/new_version ([GOOS])" + + Scenario: User agent re-announces a new ID to IMAP client and new ID is used for API calls + When user "[user:user]" connects IMAP client "1" + And IMAP client "1" announces its ID with name "name" and version "version" + When the user reports a bug + Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "name/version ([GOOS])" + When IMAP client "1" announces its ID with name "new_name" and version "new_version" + Then the user agent is "new_name/new_version ([GOOS])" + When the user reports a bug + Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "new_name/new_version ([GOOS])" + + Scenario: Apple Notes user agent is ignored after IMAP client announces its ID + When user "[user:user]" connects IMAP client "1" + And IMAP client "1" announces its ID with name "name" and version "version" + Then the user agent is "name/version ([GOOS])" + When IMAP client "1" announces its ID with name "Mac OS X Notes" and version "4.11" + Then the user agent is "name/version ([GOOS])" + From 612d1054dbb80b235a9bbf3889576e2d089353b9 Mon Sep 17 00:00:00 2001 From: Gordana Zafirova Date: Mon, 25 Nov 2024 13:25:20 +0000 Subject: [PATCH 10/19] test(BRIDGE-246): Add Settings Menu Bridge UI e2e automation tests --- .../windows_os/Results/SettingsMenuResults.cs | 118 ++++ .../windows_os/Tests/SettingsMenuTests.cs | 332 +++++++++++ .../windows_os/Windows/SettingsMenuWindow.cs | 523 ++++++++++++++++++ 3 files changed, 973 insertions(+) create mode 100644 tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs create mode 100644 tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs create mode 100644 tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs diff --git a/tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs b/tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs new file mode 100644 index 00000000..058be88d --- /dev/null +++ b/tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using ProtonMailBridge.UI.Tests.TestsHelper; +using FlaUI.Core.Input; +using System.DirectoryServices; +using System.Net; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FlaUI.Core.AutomationElements.Scrolling; +using FlaUI.Core.WindowsAPI; +using Microsoft.VisualBasic.Devices; +using NUnit.Framework.Legacy; +using ProtonMailBridge.UI.Tests.Results; +using Keyboard = FlaUI.Core.Input.Keyboard; +using Mouse = FlaUI.Core.Input.Mouse; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; +using ProtonMailBridge.UI.Tests.Windows; + +namespace ProtonMailBridge.UI.Tests.Results +{ + public class SettingsMenuResults : UIActions + { + private AutomationElement[] TextFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)); + private AutomationElement Pane => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window)); + private CheckBox AutomaticUpdates => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Automatic updates toggle"))).AsCheckBox(); + private CheckBox OpenOnStartUp => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Open on startup toggle"))).AsCheckBox(); + private CheckBox BetaAccess => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Beta access toggle"))).AsCheckBox(); + private CheckBox AlternativeRouting => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Alternative routing toggle"))).AsCheckBox(); + private CheckBox DarkMode => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Dark mode toggle"))).AsCheckBox(); + private CheckBox ShowAllMail => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Show All Mail toggle"))).AsCheckBox(); + private CheckBox CollectUsageDiagnostics => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Collect usage diagnostics toggle"))).AsCheckBox(); + private TextBox ImapPort => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("IMAP port edit"))).AsTextBox(); + private TextBox SmtpPort => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("SMTP port edit"))).AsTextBox(); + private AutomationElement[] RadioButtons => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.RadioButton)); + private RadioButton ImapStarttlsMode => RadioButtons[1].AsRadioButton(); + private RadioButton SmtpStarttlsMode => RadioButtons[3].AsRadioButton(); + private RadioButton ImapSslMode => RadioButtons[0].AsRadioButton(); + private RadioButton SmtpSslMode => RadioButtons[2].AsRadioButton(); + private TextBox CacheLocation => TextFields[9].AsTextBox(); + public SettingsMenuResults AutomaticUpdatesIsEnabledByDefault() + { + Assert.That(AutomaticUpdates.IsToggled, Is.True); + return this; + } + + public SettingsMenuResults OpenOnStartUpIsEnabledByDefault() + { + Assert.That(OpenOnStartUp.IsToggled, Is.True); + return this; + } + + public SettingsMenuResults BetaAccessIsDisabledByDefault() + { + Assert.That(BetaAccess.IsToggled, Is.False); + return this; + } + + public SettingsMenuResults AlternativeRoutingIsDisabledByDefault() + { + Assert.That(AlternativeRouting.IsToggled, Is.False); + return this; + } + + public SettingsMenuResults DarkModeIsDisabledByDefault() + { + Assert.That(DarkMode.IsToggled, Is.False); + return this; + } + public SettingsMenuResults ShowAllMailIsEnabledByDefault() + { + Assert.That(ShowAllMail.IsToggled, Is.True); + return this; + } + public SettingsMenuResults CollectUsageDiagnosticsIsEnabledByDefault() + { + Assert.That(CollectUsageDiagnostics.IsToggled, Is.True); + return this; + } + + public SettingsMenuResults VerifyDefaultPorts() + { + Assert.That(ImapPort.Patterns.Value.Pattern.Value, Is.AnyOf("1143", "1144", "1045")); + Assert.That(SmtpPort.Patterns.Value.Pattern.Value, Is.AnyOf("1025", "1026", "1027")); + return this; + } + public SettingsMenuResults VerifyDefaultConnectionMode() + { + Assert.That(ImapStarttlsMode.IsChecked, Is.True); + Assert.That(SmtpStarttlsMode.IsChecked, Is.True); + return this; + } + + public SettingsMenuResults AssertTheChangedConnectionMode() + { + Assert.That(ImapSslMode.IsChecked, Is.True); + Assert.That(SmtpSslMode.IsChecked, Is.True); + return this; + } + + public SettingsMenuResults DefaultCacheLocation() + { + string userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE"); + Assert.That(CacheLocation.Name, Is.EqualTo(userProfilePath + "\\AppData\\Roaming\\protonmail\\bridge-v3\\gluon")); + return this; + } + } +} diff --git a/tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs b/tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs new file mode 100644 index 00000000..72e04aaa --- /dev/null +++ b/tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ProtonMailBridge.UI.Tests.Results; +using ProtonMailBridge.UI.Tests.Windows; +using NUnit.Framework; +using ProtonMailBridge.UI.Tests.TestsHelper; +using FlaUI.Core.Input; +using FlaUI.Core.AutomationElements; +using FlaUI.UIA3; + +namespace ProtonMailBridge.UI.Tests.Tests +{ + [TestFixture] + public class SettingsMenuTests : TestSession + { + private readonly LoginWindow _loginWindow = new(); + private readonly HomeWindow _mainWindow = new(); + private readonly HelpMenuResult _helpMenuResult = new(); + private readonly HelpMenuWindow _helpMenuWindow = new(); + private readonly HomeResult _homeResult = new(); + private readonly SettingsMenuWindow _settingsMenuWindow = new(); + private readonly SettingsMenuResults _settingsMenuResults = new(); + + [SetUp] + public void TestInitialize() + { + LaunchApp(); + } + + [Test] + + public void OpenSettingsMenuAndSwitchBackToAccountView() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + Thread.Sleep(2000); + _homeResult.CheckIfLoggedIn(); + } + + [Test] + public void VerifyAutomaticUpdateIsEnabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuResults.AutomaticUpdatesIsEnabledByDefault(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDisableAndEnableAutomaticUpdates() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.DisableAndEnableAutomaticUpdates(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyOpenOnStartUpIsEnabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuResults.OpenOnStartUpIsEnabledByDefault(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDisableAndEnableOpenOnStartUp() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.DisableAndEnableOpenOnStartUp(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + [Test] + public void VerifyBetaAccessIsDisabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuResults.BetaAccessIsDisabledByDefault(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyEnableAndDisableBetaAccess() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.EnableAndDisableBetaAccess(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyExpandAndCollapseAdvancedSettings() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyAlternativeRoutingIsDisabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuResults.AlternativeRoutingIsDisabledByDefault(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyEnableAndDisableAlternativeRouting() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuWindow.EnableAndDisableAlternativeRouting(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDarkModeIsDisabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuResults.DarkModeIsDisabledByDefault(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void EnableAndDisableDarkMode() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuWindow.CheckEnableAndDisableDarkMode(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + [Test] + public void VerifyShowAllMailIsEnabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuResults.ShowAllMailIsEnabledByDefault(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDisableAndEnableShowAllMail() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + _settingsMenuWindow.DisableAndEnableShowAllMail(); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + [Test] + public void VerifyCollectUsageDiagnosticsIsEnabledByDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + //Thread.Sleep(3000); + _settingsMenuResults.CollectUsageDiagnosticsIsEnabledByDefault(); + Mouse.Scroll(20); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDisableAndEnableCollectUsageDiagnostics() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + _settingsMenuWindow.DisableAndEnableCollectUsageDiagnostics(); + Mouse.Scroll(20); + _settingsMenuWindow.CollapseAdvancedSettings(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDefaultImapSmtpPorts() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.OpenChangeDefaultPorts(); + Thread.Sleep(2000); + _settingsMenuResults.VerifyDefaultPorts(); + _settingsMenuWindow.CancelChangingDefaultPorts(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void ChangeAndSwitchToDefaultIMAPandSMTPports() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(5000); + _settingsMenuWindow.ChangeDefaultPorts(); + _settingsMenuWindow.SwitchBackToDefaultPorts(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void VerifyDefaultConnectionMode() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(5000); + _settingsMenuWindow.OpenChangeConnectionMode(); + _settingsMenuResults.VerifyDefaultConnectionMode(); + _settingsMenuWindow.CancelChangeConnectionMode(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void ChangeConnectionModeAndSwitchToDefault() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(5000); + _settingsMenuWindow.OpenChangeConnectionMode(); + _settingsMenuWindow.ChangeConnectionMode(); + _settingsMenuWindow.OpenChangeConnectionMode(); + _settingsMenuResults.AssertTheChangedConnectionMode(); + _settingsMenuWindow.CancelChangeConnectionMode(); + _settingsMenuWindow.OpenChangeConnectionMode(); + _settingsMenuWindow.SwitchBackToDefaultConnectionMode(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void OpenConfigureLocalCache() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.ConfigureLocalCache(); + Thread.Sleep(2000); + _settingsMenuResults.DefaultCacheLocation(); + _settingsMenuWindow.CancelToConfigureLocalCache(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void ChangeLocationSwitchBackToDefaultAndDeleteOldLocalCacheLocation() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.ConfigureLocalCache(); + Thread.Sleep(2000); + _settingsMenuWindow.ChangeAndSwitchBackLocalCacheLocation(); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void ExportTlsCertificatesVerifyExportAndDeleteTheExportFolder() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.ExportAssertDeleteTLSCertificates(); + Thread.Sleep(2000); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + [Test] + public void RepairBridge() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.VerifyRepairRestartsSync(); + Thread.Sleep(2000); + _settingsMenuWindow.ClickBackFromSettingsMenu(); + } + + [Test] + public void ResetBridge() + { + _loginWindow.SignIn(TestUserData.GetPaidUser()); + _settingsMenuWindow.ClickSettingsButton(); + _settingsMenuWindow.ExpandAdvancedSettings(); + Mouse.Scroll(-20); + Thread.Sleep(2000); + _settingsMenuWindow.VerifyResetAndRestartBridge(); + Thread.Sleep(2000); + _loginWindow.SignIn(TestUserData.GetPaidUser()); + } + + [TearDown] + public void TestCleanup() + { + _mainWindow.RemoveAccount(); + ClientCleanup(); + } + } +} diff --git a/tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs b/tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs new file mode 100644 index 00000000..2176f448 --- /dev/null +++ b/tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs @@ -0,0 +1,523 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.AutomationElements.Scrolling; +using FlaUI.Core.Definitions; +using FlaUI.Core.Input; +using FlaUI.Core.WindowsAPI; +using Microsoft.VisualBasic.Devices; +using NUnit.Framework.Legacy; +using ProtonMailBridge.UI.Tests.Results; +using ProtonMailBridge.UI.Tests.TestsHelper; +using Keyboard = FlaUI.Core.Input.Keyboard; +using Mouse = FlaUI.Core.Input.Mouse; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; +//using System.Windows.Forms; +using CheckBox = FlaUI.Core.AutomationElements.CheckBox; +using FlaUI.Core.Tools; +using System.Diagnostics; +using System.Drawing; +using System.Text.RegularExpressions; +using FlaUI.UIA3; + +namespace ProtonMailBridge.UI.Tests.Windows +{ + public class SettingsMenuWindow : UIActions + { + private static Random random = new Random(); + private const int MinPort = 49152; + private const int MaxPort = 65535; + + private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit)); + private AutomationElement[] HomeButtons => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Button)); + private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window)); + private Button EnableBetaAccessButtonInPopUp => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Enable"))).AsButton(); + private AutomationElement[] ReportProblemPane => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Pane)); + private Button SettingsButton => HomeButtons[4].AsButton(); + private Button BackToAccountViewButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Back"))).AsButton(); + private CheckBox AutomaticUpdates => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Automatic updates toggle"))).AsCheckBox(); + private CheckBox OpenOnStartUp => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Open on startup toggle"))).AsCheckBox(); + private CheckBox BetaAccess => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Beta access toggle"))).AsCheckBox(); + private TextBox AdvancedSettings => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Advanced settings"))).AsTextBox(); + private TextBox AlternativeRoutingText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Alternative routing"))).AsTextBox(); + private CheckBox AlternativeRouting => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Alternative routing toggle"))).AsCheckBox(); + private CheckBox DarkMode => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Dark mode toggle"))).AsCheckBox(); + private CheckBox ShowAllMail => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Show All Mail toggle"))).AsCheckBox(); + private Button HideAllMailFolderInPopUp => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Hide All Mail folder"))).AsButton(); + private Button ShowAllMailFolderInPopUp => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Show All Mail folder"))).AsButton(); + private CheckBox CollectUsageDiagnostics => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Collect usage diagnostics toggle"))).AsCheckBox(); + private Button ChangeDefaultPortsButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Default ports button"))).AsButton(); + private TextBox ImapPort => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("IMAP port edit"))).AsTextBox(); + private TextBox SmtpPort => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit).And(cf.ByName("SMTP port edit"))).AsTextBox(); + private Button SaveChangedPorts => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Save"))).AsButton(); + private Button CancelDefaultPorts => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Cancel"))).AsButton(); + private Button ChangeConnectionModeButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Connection mode button"))).AsButton(); + private AutomationElement[] RadioButtons => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.RadioButton)); + private RadioButton ImapStarttlsMode => RadioButtons[1].AsRadioButton(); + private RadioButton SmtpStarttlsMode => RadioButtons[3].AsRadioButton(); + private Button CancelChangeConnectionModeButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Cancel"))).AsButton(); + private RadioButton ImapSslMode => RadioButtons[0].AsRadioButton(); + private RadioButton SmtpSslMode => RadioButtons[2].AsRadioButton(); + private Button SaveChangedConnectionMode => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Save"))).AsButton(); + private Button ConfigureLocalCacheButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Local cache button"))).AsButton(); + private AutomationElement[] TextFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)); + private Button Cancel => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Cancel"))).AsButton(); + private Button ChangeLocalCacheLocationButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Current cache location button"))).AsButton(); + private TextBox CacheLocation => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Group).And(cf.ByName("Current cache location"))).FindAllDescendants(cf => cf.ByControlType(ControlType.Text))[1].AsTextBox(); + private Button ClickNewFolder => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("New folder"))).AsButton(); + private TextBox NewCreatedFolderTextBox => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByName("Shell Folder View"))).FindFirstDescendant(cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName("New folder"))).FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox(); + private Button SelectFolderButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Select Folder"))).AsButton(); + private Button SaveChangedCacheFolderLocation => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Save"))).AsButton(); + private TextBox CacheLocationIsChangedNotification => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Cache location successfully changed"))).AsTextBox(); + private Button OkCacheLocationChangedNotification => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("OK"))).AsButton(); + private Button Back => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Back"))).AsButton(); + private Button UpArrowToGoBackToPreviousFolder => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByClassName("UpBand"))).FindFirstDescendant(cf => cf.ByControlType(ControlType.Button)).AsButton(); + private Window SelectCacheLocationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window).And(cf.ByName("Select cache location"))).AsWindow(); + private Button ExportTLSCertificatesButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Export TLS certificates button"))).AsButton(); + private Window SelectDirectoryWindow => Window.FindFirstDescendant(CF => CF.ByControlType(ControlType.Window).And(CF.ByName("Select directory"))).AsWindow(); + private Button RepairBridgeButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Repair Bridge button"))).AsButton(); + private Button RepairButtonInPopUp => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Repair"))).AsButton(); + private Button ResetButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Reset Bridge button"))).AsButton(); + private Button ResetAndRestartButtonInPopUp => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Reset and restart"))).AsButton(); + private Button StartSetUpButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Start setup"))).AsButton(); + + public SettingsMenuWindow ClickSettingsButton() + { + SettingsButton.Click(); + return this; + } + + public SettingsMenuWindow ClickBackFromSettingsMenu() + { + BackToAccountViewButton.Click(); + return this; + } + + public SettingsMenuWindow DisableAndEnableAutomaticUpdates() + { + AutomaticUpdates.Click(); + Assert.That(AutomaticUpdates.IsToggled, Is.False); + Thread.Sleep(1000); + AutomaticUpdates.Click(); + Assert.That(AutomaticUpdates.IsToggled, Is.True); + return this; + } + public SettingsMenuWindow DisableAndEnableOpenOnStartUp() + { + OpenOnStartUp.Click(); + Assert.That(OpenOnStartUp.IsToggled, Is.False); + Thread.Sleep(1000); + OpenOnStartUp.Click(); + Assert.That(OpenOnStartUp.IsToggled, Is.True); + return this; + } + + public SettingsMenuWindow EnableAndDisableBetaAccess() + { + BetaAccess.Click(); + EnableBetaAccessButtonInPopUp.Click(); + Thread.Sleep(1000); + Assert.That(BetaAccess.IsToggled, Is.True); + BetaAccess.Click(); + Assert.That(BetaAccess.IsToggled, Is.False); + return this; + } + public SettingsMenuWindow ExpandAdvancedSettings() + { + AdvancedSettings.Click(); + Thread.Sleep(1000); + Assert.That(AlternativeRouting != null && AlternativeRouting.IsAvailable, Is.True); + return this; + } + + public SettingsMenuWindow CollapseAdvancedSettings() + { + AdvancedSettings.Click(); + return this; + } + public SettingsMenuWindow EnableAndDisableAlternativeRouting() + { + AlternativeRouting.Click(); + Assert.That(AlternativeRouting.IsToggled, Is.True); + Thread.Sleep(1000); + AlternativeRouting.Click(); + Assert.That(AlternativeRouting?.IsToggled, Is.False); + return this; + } + + public SettingsMenuWindow CheckEnableAndDisableDarkMode() + { + DarkMode.Click(); + Assert.That(DarkMode.IsToggled, Is.True); + Thread.Sleep(1000); + DarkMode.Click(); + Assert.That(DarkMode.IsToggled, Is.False); + return this; + } + public SettingsMenuWindow DisableAndEnableShowAllMail() + { + ShowAllMail.Click(); + HideAllMailFolderInPopUp.Click(); + Assert.That(ShowAllMail.IsToggled, Is.False); + Thread.Sleep(1000); + ShowAllMail.Click(); + Thread.Sleep(1000); + ShowAllMailFolderInPopUp.Click(); + Assert.That(ShowAllMail?.IsToggled, Is.True); + return this; + } + + public SettingsMenuWindow DisableAndEnableCollectUsageDiagnostics() + { + CollectUsageDiagnostics.Click(); + Thread.Sleep(3000); + Assert.That(CollectUsageDiagnostics.IsToggled, Is.False); + Thread.Sleep(1000); + CollectUsageDiagnostics.Click(); + Thread.Sleep(1000); + Assert.That(CollectUsageDiagnostics?.IsToggled, Is.True); + return this; + } + + public SettingsMenuWindow OpenChangeDefaultPorts() + { + ChangeDefaultPortsButton.Click(); + return this; + } + + public SettingsMenuWindow CancelChangingDefaultPorts() + { + CancelDefaultPorts.Click(); + return this; + } + private int GenerateUniqueRandomPort() + { + return random.Next(MinPort, MaxPort +1); + } + public SettingsMenuWindow ChangeDefaultPorts() + { + ChangeDefaultPortsButton.Click(); + Thread.Sleep(2000); + ImapPort.Click(); + int imapPort = GenerateUniqueRandomPort(); + int smtpPort; + + do + { + smtpPort = GenerateUniqueRandomPort(); + } while (smtpPort == imapPort); + + ImapPort.Patterns.Value.Pattern.SetValue(""); + ImapPort.Patterns.Value.Pattern.SetValue(imapPort.ToString()); + SmtpPort.Click(); + SmtpPort.Patterns.Value.Pattern.SetValue(""); + SmtpPort.Patterns.Value.Pattern.SetValue(smtpPort.ToString()); + Thread.Sleep(2000); + SaveChangedPorts.Click(); + return this; + } + + public SettingsMenuWindow SwitchBackToDefaultPorts() + { + ChangeDefaultPortsButton.Click(); + Thread.Sleep(2000); + ImapPort.Click(); + ImapPort.Patterns.Value.Pattern.SetValue(""); + ImapPort.Patterns.Value.Pattern.SetValue("1143"); + SmtpPort.Click(); + SmtpPort.Patterns.Value.Pattern.SetValue(""); + SmtpPort.Patterns.Value.Pattern.SetValue("1025"); + Thread.Sleep(2000); + SaveChangedPorts.Click(); + return this; + } + + public SettingsMenuWindow OpenChangeConnectionMode() + { + ChangeConnectionModeButton.Click(); + return this; + } + public SettingsMenuWindow CancelChangeConnectionMode() + { + CancelChangeConnectionModeButton.Click(); + return this; + } + public SettingsMenuWindow ChangeConnectionMode() + { + ImapSslMode.Click(); + SmtpSslMode.Click(); + Thread.Sleep(2000); + SaveChangedConnectionMode.Click(); + return this; + } + public SettingsMenuWindow SwitchBackToDefaultConnectionMode() + { + ImapStarttlsMode.Click(); + SmtpStarttlsMode.Click(); + Thread.Sleep(2000); + SaveChangedConnectionMode.Click(); + return this; + } + + public SettingsMenuWindow ConfigureLocalCache() + { + ConfigureLocalCacheButton.Click(); + return this; + } + public SettingsMenuWindow CancelToConfigureLocalCache() + { + Cancel.Click(); + return this; + } + + public void FocusOnSelectCacheLocationWindow() + { + if (SelectCacheLocationWindow != null) + { + SelectCacheLocationWindow.Focus(); + Console.WriteLine("Focused and interacted with 'Select cache location' window."); + } + else + { + Console.WriteLine("The 'Select cache location' window was not found."); + } + } + public void AssertOldCachefolderIsDeleted() + { + string? userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE"); + if (string.IsNullOrEmpty(userProfilePath)) + { + Console.WriteLine("User profile path not found."); + return; + } + string folderPath = Path.Combine(userProfilePath, "AppData", "Roaming", "protonmail", "bridge-v3", "gluon", "NewCacheFolder"); + try + { + if (Directory.Exists(folderPath)) + { + Directory.Delete(folderPath, recursive: true); + Console.WriteLine($"Folder '{folderPath}' deleted successfully."); + } + else + { + Console.WriteLine($"Folder '{folderPath}' does not exist."); + } + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while deleting the folder: {ex.Message}"); + } + } + + public SettingsMenuWindow ChangeAndSwitchBackLocalCacheLocation() + { + string? userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE"); + ChangeLocalCacheLocationButton.Click(); + Thread.Sleep(2000); + FocusOnSelectCacheLocationWindow(); + ClickNewFolder.Click(); + Wait.UntilInputIsProcessed(TimeSpan.FromMilliseconds(2000)); + Keyboard.TypeVirtualKeyCode(0x0D); + AutomationElement pane = Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane)); + AutomationElement pane2 = pane.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByName("Shell Folder View"))); + AutomationElement list = pane2.FindFirstDescendant(cf => cf.ByControlType(ControlType.List).And(cf.ByName("Items View"))); + AutomationElement listItem = list.FindFirstDescendant(cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName("New folder"))); + TextBox folderName = listItem.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox(); + folderName.Text = "NewCacheFolder"; + Keyboard.TypeVirtualKeyCode(0x0D); //press Enter + SelectFolderButton.Click(); + Assert.That(CacheLocation.Name, Is.EqualTo(userProfilePath + "\\AppData\\Roaming\\protonmail\\bridge-v3\\gluon\\NewCacheFolder")); + SaveChangedCacheFolderLocation.Click(); + WaitUntilElementIsVisible(() => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window)), 60); + Assert.That(CacheLocationIsChangedNotification.IsAvailable, Is.True); + OkCacheLocationChangedNotification.Click(); + Back.Click(); + Thread.Sleep(1000); + ConfigureLocalCacheButton.Click(); + ChangeLocalCacheLocationButton.Click(); + FocusOnSelectCacheLocationWindow(); + Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1)); + UpArrowToGoBackToPreviousFolder.Click(); + UpArrowToGoBackToPreviousFolder.Click(); + UpArrowToGoBackToPreviousFolder.Click(); + Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1)); + SelectFolderButton.Click(); + SaveChangedCacheFolderLocation.Click(); + WaitUntilElementIsVisible(() => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window)), 60); + OkCacheLocationChangedNotification.Click(); + Back.Click(); + Thread.Sleep(2000); + AssertOldCachefolderIsDeleted(); + Thread.Sleep(1000); + return this; + } + public void FocusOnSelectTLSCertificatesWindow() + { + if (SelectDirectoryWindow != null) + { + SelectDirectoryWindow.Focus(); + Console.WriteLine("Focused and interacted with 'Directory' window."); + } + else + { + Console.WriteLine("The 'Directory' window was not found."); + } + } + + public void AssertCertificatesAreExported() + { + string? userProfilePath = Environment.GetEnvironmentVariable("USERPROFILE"); + string folderPath = Path.Combine(userProfilePath, "TLSCertificates"); + if (string.IsNullOrEmpty(userProfilePath)) + { + Console.WriteLine("User profile path not found."); + return; + } + string certFilePath = Path.Combine(folderPath, "cert.pem"); + string keyFilePath = Path.Combine(folderPath, "key.pem"); + if (Directory.Exists(folderPath)) + { + Console.WriteLine("The TLSCertificates folder exists."); + if (File.Exists(certFilePath)) + { + Console.WriteLine("The cert.pem file exists."); + } + else + { + Console.WriteLine("The cert.pem file does not exist."); + } + if (File.Exists(keyFilePath)) + { + Console.WriteLine("The key.pem file exists."); + } + else + { + Console.WriteLine("The key.pem file does not exist."); + } + } + else + { + Console.WriteLine("The TLSCertificates folder does not exist."); + } + } + public SettingsMenuWindow ExportAssertDeleteTLSCertificates() + { + ExportTLSCertificatesButton.Click(); + Thread.Sleep(2000); + ClickNewFolder.Click(); + Wait.UntilInputIsProcessed(TimeSpan.FromMilliseconds(2000)); + Keyboard.TypeVirtualKeyCode(0x0D); + AutomationElement pane = Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane)); + AutomationElement pane2 = pane.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByName("Shell Folder View"))); + AutomationElement list = pane2.FindFirstDescendant(cf => cf.ByControlType(ControlType.List).And(cf.ByName("Items View"))); + AutomationElement listItem = list.FindFirstDescendant(cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName("New folder"))); + TextBox folderName = listItem.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox(); + folderName.Text = "TLSCertificates"; + Keyboard.TypeVirtualKeyCode(0x0D); //press Enter + Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1)); + SelectFolderButton.Click(); + Thread.Sleep(5000); + AssertCertificatesAreExported(); + Thread.Sleep(10000); + ExportTLSCertificatesButton.Click(); + FocusOnSelectTLSCertificatesWindow(); + Thread.Sleep(3000); + pane = Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane)); + pane2 = pane.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByName("Shell Folder View"))); + list = pane2.FindFirstDescendant(cf => cf.ByControlType(ControlType.List).And(cf.ByName("Items View"))); + var TLSFolder = list.FindFirstDescendant(cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName("TLSCertificates"))); + Assert.That(TLSFolder.IsAvailable, Is.True); + TLSFolder.Focus(); // Ensure the folder is selected + var boundingRectangle = TLSFolder.Properties.BoundingRectangle.Value.X; + Mouse.MoveTo(new Point(TLSFolder.Properties.BoundingRectangle.Value.X, TLSFolder.Properties.BoundingRectangle.Value.Y)); + Mouse.Click(); // Click to ensure selection + Thread.Sleep(5000); + Keyboard.TypeVirtualKeyCode(0x2E); // Press the Delete key (0x2E is the virtual key code for Delete) + Wait.UntilInputIsProcessed(TimeSpan.FromMilliseconds(1000)); // Wait for the delete action to complete + pane = Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane)); + pane2 = pane.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByName("Shell Folder View"))); + list = pane2.FindFirstDescendant(cf => cf.ByControlType(ControlType.List).And(cf.ByName("Items View"))); + var deletedFolder = list.FindFirstDescendant(cf => cf.ByControlType(ControlType.ListItem).And(cf.ByName("TLSCertificates"))); + Assert.That(deletedFolder, Is.Null, "The folder 'TLSCertificates' was not deleted successfully."); + Cancel.Click(); + return this; + } + + private void WaitUntilElementIsVisible(Func findElementFunc, int numOfSeconds) + { + TimeSpan timeout = TimeSpan.FromSeconds(numOfSeconds); + Stopwatch stopwatch = Stopwatch.StartNew(); + + + while (stopwatch.Elapsed < timeout) + { + //if element is visible the processing is completed + var element = findElementFunc(); + if (element != null) + { + return; + } + + Wait.UntilInputIsProcessed(); + Thread.Sleep(500); + } + + } + public SettingsMenuWindow VerifyRepairRestartsSync() + { + RepairBridgeButton.Click(); + RepairButtonInPopUp.Click(); + bool syncRestarted = WaitForCondition(() => + { + string syncStatus = GetSyncStatus(Window); + return !string.IsNullOrEmpty(syncStatus) && syncStatus.Contains("Synchronizing (0%)"); + }, TimeSpan.FromSeconds(30)); // Adjust timeout as needed + + Assert.That(syncRestarted, Is.True, "Sync did not restart after repair."); + return this; + } + private string GetSyncStatus(AutomationElement window) + { + var syncStatusElement = window.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)).FirstOrDefault(el => + { + string name = el.Name; + return !string.IsNullOrEmpty(name) && + name.StartsWith("Synchronizing (") && + name.EndsWith("%)"); + }); + + return syncStatusElement?.AsLabel()?.Text ?? string.Empty; + } + private bool WaitForCondition(Func condition, TimeSpan timeout, int pollingIntervalMs = 500) + { + var endTime = DateTime.Now + timeout; + while (DateTime.Now < endTime) + { + if (condition()) + return true; + Thread.Sleep(pollingIntervalMs); + } + return false; + } + + public SettingsMenuWindow VerifyResetAndRestartBridge() + { + ResetButton.Click(); + ResetAndRestartButtonInPopUp.Click(); + Thread.Sleep(5000); + LaunchApp(); + Window.Focus(); + Thread.Sleep(5000); + Assert.That(StartSetUpButton.IsAvailable, Is.True); + StartSetUpButton.Click(); + return this; + } + } +} \ No newline at end of file From 80d556343e5083530d3fa5d2497909ca2d4543b5 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Tue, 26 Nov 2024 16:02:43 +0100 Subject: [PATCH 11/19] fix(BRIDGE-256): fix reversed order of headers with multiple values. --- pkg/message/build.go | 5 +++-- pkg/message/build_framework_test.go | 5 +++++ pkg/message/header_test.go | 20 +++++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pkg/message/build.go b/pkg/message/build.go index c9c82ced..8f81f812 100644 --- a/pkg/message/build.go +++ b/pkg/message/build.go @@ -531,11 +531,12 @@ func toMessageHeader(hdr proton.Headers) message.Header { // go-message's message.Header are in reversed order (you should only add fields at the top, so storing in reverse order offer faster performances). for i := len(hdr.Order) - 1; i >= 0; i-- { key := hdr.Order[i] - for _, val := range hdr.Values[key] { + values := hdr.Values[key] + for j := len(values) - 1; j >= 0; j-- { // Using AddRaw instead of Add to save key-value pair as byte buffer within Header. // This buffer is used latter on in message writer to construct message and avoid crash // when key length is more than 76 characters long. - res.AddRaw([]byte(key + ": " + val + "\r\n")) + res.AddRaw([]byte(key + ": " + values[j] + "\r\n")) } } diff --git a/pkg/message/build_framework_test.go b/pkg/message/build_framework_test.go index 18cb77f8..ba065a13 100644 --- a/pkg/message/build_framework_test.go +++ b/pkg/message/build_framework_test.go @@ -101,6 +101,11 @@ func newTestMessageFromRFC822(t *testing.T, literal []byte) proton.Message { var parsedHeaders proton.Headers parsedHeaders.Values = make(map[string][]string) h.Entries(func(key, val string) { + currentVal, ok := parsedHeaders.Values[key] + if ok { + parsedHeaders.Values[key] = append(currentVal, val) + return + } parsedHeaders.Values[key] = []string{val} parsedHeaders.Order = append(parsedHeaders.Order, key) }) diff --git a/pkg/message/header_test.go b/pkg/message/header_test.go index 3d916557..db068a62 100644 --- a/pkg/message/header_test.go +++ b/pkg/message/header_test.go @@ -145,6 +145,9 @@ From: Dummy Recipient Date: Tue, 15 Oct 2024 07:54:39 +0000 Mime-Version: 1.0 Content-Type: multipart/mixed;boundary=---------------------a136fc3851075ca3f022f5c3ec6bf8f5 +X-Attached: image1.jpg +X-Attached: image2.jpg +X-Attached: image3.jpg Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me> X-Pm-Spamscore: 0 Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000 @@ -178,7 +181,7 @@ lorem`) lines := strings.Split(str, "\r\n") // Check we have the expected order - require.Equal(t, len(lines), 17) + require.Equal(t, len(lines), 20) // The fields added or modified are at the top require.True(t, strings.HasPrefix(lines[0], "Content-Type: multipart/mixed;boundary=")) // we changed the boundary @@ -194,10 +197,13 @@ lorem`) require.Equal(t, `Subject: header test`, lines[8]) require.Equal(t, `Date: Tue, 15 Oct 2024 07:54:39 +0000`, lines[9]) require.Equal(t, `Mime-Version: 1.0`, lines[10]) - require.Equal(t, `Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>`, lines[11]) - require.Equal(t, `X-Pm-Spamscore: 0`, lines[12]) - require.Equal(t, `Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000`, lines[13]) - require.Equal(t, `X-Original-To: test@proton.me`, lines[14]) - require.Equal(t, `Return-Path: `, lines[15]) - require.Equal(t, `Delivered-To: test@proton.me`, lines[16]) + require.Equal(t, `X-Attached: image1.jpg`, lines[11]) + require.Equal(t, `X-Attached: image2.jpg`, lines[12]) + require.Equal(t, `X-Attached: image3.jpg`, lines[13]) + require.Equal(t, `Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>`, lines[14]) + require.Equal(t, `X-Pm-Spamscore: 0`, lines[15]) + require.Equal(t, `Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000`, lines[16]) + require.Equal(t, `X-Original-To: test@proton.me`, lines[17]) + require.Equal(t, `Return-Path: `, lines[18]) + require.Equal(t, `Delivered-To: test@proton.me`, lines[19]) } From 5fbe94c5597c1b01be843b05eee68e4d3cbd8e0b Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Tue, 29 Oct 2024 10:47:33 +0100 Subject: [PATCH 12/19] chore: Erasmus Bridge 3.15.0 changelog. --- Changelog.md | 24 ++++++++++++++++++++++++ Makefile | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 44c72f56..5d663793 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,30 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) +## Erasmus Bridge 3.15.0 + +### Added +* BRIDGE-238: Added host information to sentry events; new sentry event for keychain issues. +* BRIDGE-236: Added SMTP observability metrics. +* BRIDGE-217: Added missing parameter to the CLI help command. +* BRIDGE-234: Add accessibility name in QML for UI automation. +* BRIDGE-232: Test: Add Home Menu Bridge UI e2e automation tests. +* BRIDGE-220: Test: Add Bridge E2E UI login/logout tests for Windows. + +### Changed +* BRIDGE-228: Removed sentry events. +* BRIDGE-218: Observability adapter; gluon observability metrics and tests. +* BRIDGE-215: Tweak wording on macOS profile install page. +* BRIDGE-131: Test: Integration tests for messages from Proton <-> Gmail. +* BRIDGE-142: Bridge icon can be removed from the menu bar on macOS. + +### Fixed +* BRIDGE-240: Fix for running against Qt 6.8 (contribution of GitHub user Cimbali). +* BRIDGE-231: Fix reversed header order in messages. +* BRIDGE-235: Fix compilation of Bridge GUI Tester on Windows. +* BRIDGE-120: Use appropriate address key when importing / saving draft. + + ## Dragon Bridge 3.14.0 ### Changed diff --git a/Makefile b/Makefile index 2c8daba3..9e373cdb 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .) .PHONY: build build-gui build-nogui build-launcher versioner hasher # Keep version hardcoded so app build works also without Git repository. -BRIDGE_APP_VERSION?=3.14.0+git +BRIDGE_APP_VERSION?=3.15.0+git APP_VERSION:=${BRIDGE_APP_VERSION} APP_FULL_NAME:=Proton Mail Bridge APP_VENDOR:=Proton AG From 3f78f4d672528cff56899b7ecfffcb71b86b5d63 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 29 Nov 2024 09:14:29 +0100 Subject: [PATCH 13/19] feat(BRIDGE-281): disable keychain test on macOS. --- internal/app/app.go | 49 ++++---------------- internal/app/app_test.go | 64 -------------------------- pkg/keychain/helper_darwin.go | 15 ++---- pkg/keychain/helper_linux.go | 2 +- pkg/keychain/helper_windows.go | 2 +- pkg/keychain/keychain.go | 10 ++-- pkg/keychain/keychain_test.go | 2 +- utils/bridge-rollout/bridge-rollout.go | 4 +- utils/vault-editor/main.go | 4 +- 9 files changed, 25 insertions(+), 127 deletions(-) delete mode 100644 internal/app/app_test.go diff --git a/internal/app/app.go b/internal/app/app.go index b26d6cc2..4aa3e4c7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -97,18 +97,21 @@ const ( appShortName = "bridge" ) +// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup. var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals Name: flagEnableKeychainTest, - Usage: "Enable the keychain test for the current and future executions of the application", + Usage: "This flag is deprecated and does nothing", Value: false, DisableDefaultText: true, -} //nolint:gochecknoglobals + Hidden: true, +} var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals Name: flagDisableKeychainTest, - Usage: "Disable the keychain test for the current and future executions of the application", + Usage: "This flag is deprecated and does nothing", Value: false, DisableDefaultText: true, + Hidden: true, } func New() *cli.App { @@ -212,6 +215,7 @@ func New() *cli.App { if onMacOS() { // The two flags below were introduced for BRIDGE-116, and are available only on macOS. + // They have been later removed fro BRIDGE-281. app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest) } @@ -283,8 +287,7 @@ func run(c *cli.Context) error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error { // Look for available keychains - skipKeychainTest := checkSkipKeychainTest(c, settings) - return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error { + return WithKeychainList(crashHandler, func(keychains *keychain.List) error { // Unlock the encrypted vault. return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { if !v.Migrated() { @@ -548,11 +551,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error { } // WithKeychainList init the list of usable keychains. -func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error { +func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error { logrus.Debug("Creating keychain list") defer logrus.Debug("Keychain list stop") defer async.HandlePanic(panicHandler) - return fn(keychain.NewList(skipKeychainTest)) + return fn(keychain.NewList()) } func setDeviceCookies(jar *cookies.Jar) error { @@ -573,38 +576,6 @@ func setDeviceCookies(jar *cookies.Jar) error { return nil } -func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool { - if !onMacOS() { - return false - } - - enable := c.Bool(flagEnableKeychainTest) - disable := c.Bool(flagDisableKeychainTest) - - skip, err := vault.GetShouldSkipKeychainTest(settingsDir) - if err != nil { - logrus.WithError(err).Error("Could not load keychain settings.") - } - - if (!enable) && (!disable) { - return skip - } - - // if both switches are passed, 'enable' has priority - if disable { - skip = true - } - if enable { - skip = false - } - - if err := vault.SetShouldSkipKeychainTest(settingsDir, skip); err != nil { - logrus.WithError(err).Error("Could not save keychain settings.") - } - - return skip -} - func onMacOS() bool { return runtime.GOOS == "darwin" } diff --git a/internal/app/app_test.go b/internal/app/app_test.go deleted file mode 100644 index 88d549e4..00000000 --- a/internal/app/app_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2024 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 . - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -func TestCheckSkipKeychainTest(t *testing.T) { - var expectedResult bool - dir := t.TempDir() - app := cli.App{ - Flags: []cli.Flag{ - cliFlagEnableKeychainTest, - cliFlagDisableKeychainTest, - }, - Action: func(c *cli.Context) error { - require.Equal(t, expectedResult, checkSkipKeychainTest(c, dir)) - return nil - }, - } - - noArgs := []string{"appName"} - enableArgs := []string{"appName", "-" + flagEnableKeychainTest} - disableArgs := []string{"appName", "-" + flagDisableKeychainTest} - bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest} - - onMac := onMacOS() - - expectedResult = false - require.NoError(t, app.Run(noArgs)) - - expectedResult = onMac - require.NoError(t, app.Run(disableArgs)) - require.NoError(t, app.Run(noArgs)) - - expectedResult = false - require.NoError(t, app.Run(enableArgs)) - require.NoError(t, app.Run(noArgs)) - - expectedResult = onMac - require.NoError(t, app.Run(disableArgs)) - - expectedResult = false - require.NoError(t, app.Run(bothArgs)) -} diff --git a/pkg/keychain/helper_darwin.go b/pkg/keychain/helper_darwin.go index 867d2fec..77df8d02 100644 --- a/pkg/keychain/helper_darwin.go +++ b/pkg/keychain/helper_darwin.go @@ -31,21 +31,12 @@ const ( MacOSKeychain = "macos-keychain" ) -func listHelpers(skipKeychainTest bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) // MacOS always provides a keychain. - if skipKeychainTest { - logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test") - helpers[MacOSKeychain] = newMacOSHelper - } else { - if isUsable(newMacOSHelper("")) { - helpers[MacOSKeychain] = newMacOSHelper - logrus.WithField("keychain", "MacOSKeychain").Info("Keychain is usable.") - } else { - logrus.WithField("keychain", "MacOSKeychain").Debug("Keychain is not available.") - } - } + logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test") + helpers[MacOSKeychain] = newMacOSHelper // Use MacOSKeychain by default. return helpers, MacOSKeychain diff --git a/pkg/keychain/helper_linux.go b/pkg/keychain/helper_linux.go index 7a395bc6..ce531faa 100644 --- a/pkg/keychain/helper_linux.go +++ b/pkg/keychain/helper_linux.go @@ -31,7 +31,7 @@ const ( SecretServiceDBus = "secret-service-dbus" ) -func listHelpers(_ bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) if isUsable(newDBusHelper("")) { diff --git a/pkg/keychain/helper_windows.go b/pkg/keychain/helper_windows.go index 506601d9..6ac141ff 100644 --- a/pkg/keychain/helper_windows.go +++ b/pkg/keychain/helper_windows.go @@ -25,7 +25,7 @@ import ( const WindowsCredentials = "windows-credentials" -func listHelpers(_ bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) // Windows always provides a keychain. if isUsable(newWinCredHelper("")) { diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index d734f398..efa0236b 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -62,9 +62,9 @@ type List struct { // NewList checks availability of every keychains detected on the User Operating System // This will ask the user to unlock keychain(s) to check their usability. // This should only be called once. -func NewList(skipKeychainTest bool) *List { +func NewList() *List { var list = List{locker: &sync.Mutex{}} - list.helpers, list.defaultHelper = listHelpers(skipKeychainTest) + list.helpers, list.defaultHelper = listHelpers() return &list } @@ -210,7 +210,7 @@ func (kc *Keychain) secretURL(userID string) string { } // isUsable returns whether the credentials helper is usable. -func isUsable(helper credentials.Helper, err error) bool { +func isUsable(helper credentials.Helper, err error) bool { //nolint:unused l := logrus.WithField("helper", reflect.TypeOf(helper)) if err != nil { @@ -240,7 +240,7 @@ func isUsable(helper credentials.Helper, err error) bool { return true } -func getTestCredentials() *credentials.Credentials { +func getTestCredentials() *credentials.Credentials { //nolint:unused // On macOS, a handful of users experience failures of the test credentials. if runtime.GOOS == "darwin" { return &credentials.Credentials{ @@ -257,7 +257,7 @@ func getTestCredentials() *credentials.Credentials { } } -func retry(condition func() error) error { +func retry(condition func() error) error { //nolint:unused var maxRetry = 5 for r := 0; ; r++ { err := condition() diff --git a/pkg/keychain/keychain_test.go b/pkg/keychain/keychain_test.go index 62ee659d..becbb4d3 100644 --- a/pkg/keychain/keychain_test.go +++ b/pkg/keychain/keychain_test.go @@ -117,7 +117,7 @@ func TestInsertReadRemove(t *testing.T) { func TestIsErrKeychainNoItem(t *testing.T) { r := require.New(t) - helpers := NewList(false).GetHelpers() + helpers := NewList().GetHelpers() for helperName := range helpers { kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName) diff --git a/utils/bridge-rollout/bridge-rollout.go b/utils/bridge-rollout/bridge-rollout.go index becf8b4f..fc9d4b7d 100644 --- a/utils/bridge-rollout/bridge-rollout.go +++ b/utils/bridge-rollout/bridge-rollout.go @@ -61,7 +61,7 @@ func main() { func getRollout(_ *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error { fmt.Println(vault.GetUpdateRollout()) return nil @@ -72,7 +72,7 @@ func getRollout(_ *cli.Context) error { func setRollout(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error { clamped := max(0.0, min(1.0, c.Float64("value"))) if err := vault.SetUpdateRollout(clamped); err != nil { diff --git a/utils/vault-editor/main.go b/utils/vault-editor/main.go index 91d3bfdc..8acebe9d 100644 --- a/utils/vault-editor/main.go +++ b/utils/vault-editor/main.go @@ -51,7 +51,7 @@ func main() { func readAction(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error { if _, err := os.Stdout.Write(vault.ExportJSON()); err != nil { return fmt.Errorf("failed to write vault: %w", err) @@ -65,7 +65,7 @@ func readAction(c *cli.Context) error { func writeAction(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error { b, err := io.ReadAll(os.Stdin) if err != nil { From dd2448f35a4b79a50c56c4d58d54751dd43b2b9f Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Tue, 3 Dec 2024 18:22:40 +0100 Subject: [PATCH 14/19] fix(BRIDGE-266): changed heartbeat measurement group --- internal/telemetry/heartbeat.go | 2 +- internal/telemetry/heartbeat_test.go | 2 +- tests/features/bridge/heartbeat.feature | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/telemetry/heartbeat.go b/internal/telemetry/heartbeat.go index f682af03..fa492125 100644 --- a/internal/telemetry/heartbeat.go +++ b/internal/telemetry/heartbeat.go @@ -34,7 +34,7 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke log: logrus.WithField("pkg", "telemetry"), manager: manager, metrics: HeartbeatData{ - MeasurementGroup: "bridge.any.usage", + MeasurementGroup: "bridge.any.heartbeat", Event: "bridge_heartbeat_new", Dimensions: NewHeartbeatDimensions(), }, diff --git a/internal/telemetry/heartbeat_test.go b/internal/telemetry/heartbeat_test.go index 101282ad..77cf526f 100644 --- a/internal/telemetry/heartbeat_test.go +++ b/internal/telemetry/heartbeat_test.go @@ -32,7 +32,7 @@ import ( func TestHeartbeat_default_heartbeat(t *testing.T) { withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) { data := telemetry.HeartbeatData{ - MeasurementGroup: "bridge.any.usage", + MeasurementGroup: "bridge.any.heartbeat", Event: "bridge_heartbeat_new", Values: telemetry.HeartbeatValues{ NumberConnectedAccounts: 1, diff --git a/tests/features/bridge/heartbeat.feature b/tests/features/bridge/heartbeat.feature index 25aa5b40..ba225fef 100644 --- a/tests/features/bridge/heartbeat.feature +++ b/tests/features/bridge/heartbeat.feature @@ -16,7 +16,7 @@ Feature: Send Telemetry Heartbeat Then bridge eventually sends the following heartbeat: """ { - "MeasurementGroup": "bridge.any.usage", + "MeasurementGroup": "bridge.any.heartbeat", "Event": "bridge_heartbeat_new", "Values": { "NumberConnectedAccounts": 1, @@ -62,7 +62,7 @@ Feature: Send Telemetry Heartbeat Then bridge eventually sends the following heartbeat: """ { - "MeasurementGroup": "bridge.any.usage", + "MeasurementGroup": "bridge.any.heartbeat", "Event": "bridge_heartbeat_new", "Values": { "NumberConnectedAccounts": 1, @@ -102,7 +102,7 @@ Feature: Send Telemetry Heartbeat Then bridge eventually sends the following heartbeat: """ { - "MeasurementGroup": "bridge.any.usage", + "MeasurementGroup": "bridge.any.heartbeat", "Event": "bridge_heartbeat_new", "Values": { "NumberConnectedAccounts": 1, @@ -142,7 +142,7 @@ Feature: Send Telemetry Heartbeat Then bridge eventually sends the following heartbeat: """ { - "MeasurementGroup": "bridge.any.usage", + "MeasurementGroup": "bridge.any.heartbeat", "Event": "bridge_heartbeat_new", "Values": { "NumberConnectedAccounts": 3, @@ -180,7 +180,7 @@ Feature: Send Telemetry Heartbeat Then bridge eventually sends the following heartbeat: """ { - "MeasurementGroup": "bridge.any.usage", + "MeasurementGroup": "bridge.any.heartbeat", "Event": "bridge_heartbeat_new", "Values": { "NumberConnectedAccounts": 1, From 7cf3b6fb7bb20addb5f4980ff688ef93335447d5 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 29 Nov 2024 09:14:29 +0100 Subject: [PATCH 15/19] feat(BRIDGE-281): disable keychain test on macOS. (cherry picked from commit 3f78f4d672528cff56899b7ecfffcb71b86b5d63) --- internal/app/app.go | 49 ++++---------------- internal/app/app_test.go | 64 -------------------------- pkg/keychain/helper_darwin.go | 15 ++---- pkg/keychain/helper_linux.go | 2 +- pkg/keychain/helper_windows.go | 2 +- pkg/keychain/keychain.go | 10 ++-- pkg/keychain/keychain_test.go | 2 +- utils/bridge-rollout/bridge-rollout.go | 4 +- utils/vault-editor/main.go | 4 +- 9 files changed, 25 insertions(+), 127 deletions(-) delete mode 100644 internal/app/app_test.go diff --git a/internal/app/app.go b/internal/app/app.go index be0ce6fe..edc7697a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -97,18 +97,21 @@ const ( appShortName = "bridge" ) +// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup. var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals Name: flagEnableKeychainTest, - Usage: "Enable the keychain test for the current and future executions of the application", + Usage: "This flag is deprecated and does nothing", Value: false, DisableDefaultText: true, -} //nolint:gochecknoglobals + Hidden: true, +} var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals Name: flagDisableKeychainTest, - Usage: "Disable the keychain test for the current and future executions of the application", + Usage: "This flag is deprecated and does nothing", Value: false, DisableDefaultText: true, + Hidden: true, } func New() *cli.App { @@ -211,6 +214,7 @@ func New() *cli.App { if onMacOS() { // The two flags below were introduced for BRIDGE-116, and are available only on macOS. + // They have been later removed fro BRIDGE-281. app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest) } @@ -282,8 +286,7 @@ func run(c *cli.Context) error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error { // Look for available keychains - skipKeychainTest := checkSkipKeychainTest(c, settings) - return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error { + return WithKeychainList(crashHandler, func(keychains *keychain.List) error { // Unlock the encrypted vault. return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { if !v.Migrated() { @@ -547,11 +550,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error { } // WithKeychainList init the list of usable keychains. -func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error { +func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error { logrus.Debug("Creating keychain list") defer logrus.Debug("Keychain list stop") defer async.HandlePanic(panicHandler) - return fn(keychain.NewList(skipKeychainTest)) + return fn(keychain.NewList()) } func setDeviceCookies(jar *cookies.Jar) error { @@ -572,38 +575,6 @@ func setDeviceCookies(jar *cookies.Jar) error { return nil } -func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool { - if !onMacOS() { - return false - } - - enable := c.Bool(flagEnableKeychainTest) - disable := c.Bool(flagDisableKeychainTest) - - skip, err := vault.GetShouldSkipKeychainTest(settingsDir) - if err != nil { - logrus.WithError(err).Error("Could not load keychain settings.") - } - - if (!enable) && (!disable) { - return skip - } - - // if both switches are passed, 'enable' has priority - if disable { - skip = true - } - if enable { - skip = false - } - - if err := vault.SetShouldSkipKeychainTest(settingsDir, skip); err != nil { - logrus.WithError(err).Error("Could not save keychain settings.") - } - - return skip -} - func onMacOS() bool { return runtime.GOOS == "darwin" } diff --git a/internal/app/app_test.go b/internal/app/app_test.go deleted file mode 100644 index 88d549e4..00000000 --- a/internal/app/app_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2024 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 . - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -func TestCheckSkipKeychainTest(t *testing.T) { - var expectedResult bool - dir := t.TempDir() - app := cli.App{ - Flags: []cli.Flag{ - cliFlagEnableKeychainTest, - cliFlagDisableKeychainTest, - }, - Action: func(c *cli.Context) error { - require.Equal(t, expectedResult, checkSkipKeychainTest(c, dir)) - return nil - }, - } - - noArgs := []string{"appName"} - enableArgs := []string{"appName", "-" + flagEnableKeychainTest} - disableArgs := []string{"appName", "-" + flagDisableKeychainTest} - bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest} - - onMac := onMacOS() - - expectedResult = false - require.NoError(t, app.Run(noArgs)) - - expectedResult = onMac - require.NoError(t, app.Run(disableArgs)) - require.NoError(t, app.Run(noArgs)) - - expectedResult = false - require.NoError(t, app.Run(enableArgs)) - require.NoError(t, app.Run(noArgs)) - - expectedResult = onMac - require.NoError(t, app.Run(disableArgs)) - - expectedResult = false - require.NoError(t, app.Run(bothArgs)) -} diff --git a/pkg/keychain/helper_darwin.go b/pkg/keychain/helper_darwin.go index 867d2fec..77df8d02 100644 --- a/pkg/keychain/helper_darwin.go +++ b/pkg/keychain/helper_darwin.go @@ -31,21 +31,12 @@ const ( MacOSKeychain = "macos-keychain" ) -func listHelpers(skipKeychainTest bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) // MacOS always provides a keychain. - if skipKeychainTest { - logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test") - helpers[MacOSKeychain] = newMacOSHelper - } else { - if isUsable(newMacOSHelper("")) { - helpers[MacOSKeychain] = newMacOSHelper - logrus.WithField("keychain", "MacOSKeychain").Info("Keychain is usable.") - } else { - logrus.WithField("keychain", "MacOSKeychain").Debug("Keychain is not available.") - } - } + logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test") + helpers[MacOSKeychain] = newMacOSHelper // Use MacOSKeychain by default. return helpers, MacOSKeychain diff --git a/pkg/keychain/helper_linux.go b/pkg/keychain/helper_linux.go index 7a395bc6..ce531faa 100644 --- a/pkg/keychain/helper_linux.go +++ b/pkg/keychain/helper_linux.go @@ -31,7 +31,7 @@ const ( SecretServiceDBus = "secret-service-dbus" ) -func listHelpers(_ bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) if isUsable(newDBusHelper("")) { diff --git a/pkg/keychain/helper_windows.go b/pkg/keychain/helper_windows.go index 506601d9..6ac141ff 100644 --- a/pkg/keychain/helper_windows.go +++ b/pkg/keychain/helper_windows.go @@ -25,7 +25,7 @@ import ( const WindowsCredentials = "windows-credentials" -func listHelpers(_ bool) (Helpers, string) { +func listHelpers() (Helpers, string) { helpers := make(Helpers) // Windows always provides a keychain. if isUsable(newWinCredHelper("")) { diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index d734f398..efa0236b 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -62,9 +62,9 @@ type List struct { // NewList checks availability of every keychains detected on the User Operating System // This will ask the user to unlock keychain(s) to check their usability. // This should only be called once. -func NewList(skipKeychainTest bool) *List { +func NewList() *List { var list = List{locker: &sync.Mutex{}} - list.helpers, list.defaultHelper = listHelpers(skipKeychainTest) + list.helpers, list.defaultHelper = listHelpers() return &list } @@ -210,7 +210,7 @@ func (kc *Keychain) secretURL(userID string) string { } // isUsable returns whether the credentials helper is usable. -func isUsable(helper credentials.Helper, err error) bool { +func isUsable(helper credentials.Helper, err error) bool { //nolint:unused l := logrus.WithField("helper", reflect.TypeOf(helper)) if err != nil { @@ -240,7 +240,7 @@ func isUsable(helper credentials.Helper, err error) bool { return true } -func getTestCredentials() *credentials.Credentials { +func getTestCredentials() *credentials.Credentials { //nolint:unused // On macOS, a handful of users experience failures of the test credentials. if runtime.GOOS == "darwin" { return &credentials.Credentials{ @@ -257,7 +257,7 @@ func getTestCredentials() *credentials.Credentials { } } -func retry(condition func() error) error { +func retry(condition func() error) error { //nolint:unused var maxRetry = 5 for r := 0; ; r++ { err := condition() diff --git a/pkg/keychain/keychain_test.go b/pkg/keychain/keychain_test.go index 62ee659d..becbb4d3 100644 --- a/pkg/keychain/keychain_test.go +++ b/pkg/keychain/keychain_test.go @@ -117,7 +117,7 @@ func TestInsertReadRemove(t *testing.T) { func TestIsErrKeychainNoItem(t *testing.T) { r := require.New(t) - helpers := NewList(false).GetHelpers() + helpers := NewList().GetHelpers() for helperName := range helpers { kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName) diff --git a/utils/bridge-rollout/bridge-rollout.go b/utils/bridge-rollout/bridge-rollout.go index becf8b4f..fc9d4b7d 100644 --- a/utils/bridge-rollout/bridge-rollout.go +++ b/utils/bridge-rollout/bridge-rollout.go @@ -61,7 +61,7 @@ func main() { func getRollout(_ *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error { fmt.Println(vault.GetUpdateRollout()) return nil @@ -72,7 +72,7 @@ func getRollout(_ *cli.Context) error { func setRollout(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error { clamped := max(0.0, min(1.0, c.Float64("value"))) if err := vault.SetUpdateRollout(clamped); err != nil { diff --git a/utils/vault-editor/main.go b/utils/vault-editor/main.go index 91d3bfdc..8acebe9d 100644 --- a/utils/vault-editor/main.go +++ b/utils/vault-editor/main.go @@ -51,7 +51,7 @@ func main() { func readAction(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error { if _, err := os.Stdout.Write(vault.ExportJSON()); err != nil { return fmt.Errorf("failed to write vault: %w", err) @@ -65,7 +65,7 @@ func readAction(c *cli.Context) error { func writeAction(c *cli.Context) error { return app.WithLocations(func(locations *locations.Locations) error { - return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error { + return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error { return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error { b, err := io.ReadAll(os.Stdin) if err != nil { From 120a7b362608e536502e3c6d79f7df0db82c0456 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Wed, 4 Dec 2024 14:44:25 +0100 Subject: [PATCH 16/19] chore: Erasmus Bridge 3.15.1 changelog. --- Changelog.md | 6 ++++++ Makefile | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 5d663793..78ef49a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,12 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) +## Erasmus Bridge 3.15.1 + +### Changed +* BRIDGE-281: Disable keychain test on macOS. + + ## Erasmus Bridge 3.15.0 ### Added diff --git a/Makefile b/Makefile index 9e373cdb..de2d9a44 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .) .PHONY: build build-gui build-nogui build-launcher versioner hasher # Keep version hardcoded so app build works also without Git repository. -BRIDGE_APP_VERSION?=3.15.0+git +BRIDGE_APP_VERSION?=3.15.1+git APP_VERSION:=${BRIDGE_APP_VERSION} APP_FULL_NAME:=Proton Mail Bridge APP_VENDOR:=Proton AG From ef779a23c1438d0738bdd4359ce42277736d727e Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Fri, 29 Nov 2024 15:25:26 +0100 Subject: [PATCH 17/19] chore: fix linter issues. --- internal/bridge/bridge.go | 8 -------- internal/network/proton.go | 4 ++-- internal/services/syncservice/stage_download.go | 10 +++++----- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index fc9a3b00..36d74020 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -636,14 +636,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) { }, nil } -func min(a, b time.Duration) time.Duration { //nolint:predeclared - if a < b { - return a - } - - return b -} - func (bridge *Bridge) HasAPIConnection() bool { return bridge.api.GetStatus() == proton.StatusUp } diff --git a/internal/network/proton.go b/internal/network/proton.go index 29aae338..52cf489d 100644 --- a/internal/network/proton.go +++ b/internal/network/proton.go @@ -31,8 +31,8 @@ type CoolDownProvider interface { Reset() } -func jitter(max int) time.Duration { //nolint:predeclared - return time.Duration(rand.Intn(max)) * time.Second //nolint:gosec +func jitter(maxValue int) time.Duration { + return time.Duration(rand.Intn(maxValue)) * time.Second //nolint:gosec } type ExpCoolDown struct { diff --git a/internal/services/syncservice/stage_download.go b/internal/services/syncservice/stage_download.go index 17144cd0..dff87c87 100644 --- a/internal/services/syncservice/stage_download.go +++ b/internal/services/syncservice/stage_download.go @@ -231,7 +231,7 @@ func downloadAttachment(ctx context.Context, cache *DownloadCache, client APICli } type DownloadRateModifier interface { - Apply(wasSuccess bool, current int, max int) int //nolint:predeclared + Apply(wasSuccess bool, currentValue int, maxValue int) int } func autoDownloadRate[T any, R any]( @@ -285,14 +285,14 @@ func autoDownloadRate[T any, R any]( type DefaultDownloadRateModifier struct{} -func (d DefaultDownloadRateModifier) Apply(wasSuccess bool, current int, max int) int { //nolint:predeclared +func (d DefaultDownloadRateModifier) Apply(wasSuccess bool, currentValue int, maxValue int) int { if !wasSuccess { return 2 } - parallelTasks := current * 2 - if parallelTasks > max { - parallelTasks = max + parallelTasks := currentValue * 2 + if parallelTasks > maxValue { + parallelTasks = maxValue } return parallelTasks From a4772ee4e02d08e0c7b7b25bc3500af41d38e865 Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Thu, 12 Dec 2024 10:44:59 +0100 Subject: [PATCH 18/19] chore: added CODEOWNER group --- .gitlab/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitlab/CODEOWNERS diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS new file mode 100644 index 00000000..602df74f --- /dev/null +++ b/.gitlab/CODEOWNERS @@ -0,0 +1 @@ +* @go/bridge-ppl/devs \ No newline at end of file From 0e6df4ce73c068c8c4bf2c9c44246c5d35f574eb Mon Sep 17 00:00:00 2001 From: Gabor Meszaros Date: Mon, 16 Dec 2024 10:47:24 +0000 Subject: [PATCH 19/19] chore: Prepare for issue tracker removal --- .../ISSUE_TEMPLATE/general-issue-template.md | 41 ---------- CONTRIBUTING.md | 82 +++++++++++++++++-- 2 files changed, 75 insertions(+), 48 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/general-issue-template.md diff --git a/.github/ISSUE_TEMPLATE/general-issue-template.md b/.github/ISSUE_TEMPLATE/general-issue-template.md deleted file mode 100644 index 6eaaefe3..00000000 --- a/.github/ISSUE_TEMPLATE/general-issue-template.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: General issue template -about: Template for detailed report of issues -title: '' -labels: '' -assignees: '' - ---- - -Issue tracker is ONLY used for reporting bugs with technical details. "It doesn't work" or new features should be discussed with our customer support. Please use bug report function in Bridge or contact bridge@protonmail.ch. - - -## Expected Behavior - - -## Current Behavior - - -## Possible Solution - - -## Steps to Reproduce - - -1. -2. -3. -4. - -## Version Information - - -## Context (Environment) - - - -## Detailed Description - - -## Possible Implementation - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea729c2f..62cc0b84 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,78 @@ -# Contribution Policy +# Contributing guidelines + +The following document describes how to contribute to the project. In this context, contribution does not only mean code contribution but also reporting issues, requesting new features, or just asking for help. + +## Reporting issues + +In case you experience issues while using the application, our request is to contact Proton customer support directly. + +The benefits of using Proton customer support are + +- Available 24/7/365. +- Provides priority support based on subscription type. +- Will escalate the issue to the developers every time it becomes too technical or they do not know the answer to a question. +- Easier to detect systematic issues by connecting similar reports. +- Possible to quickly derive frequency of an issue. +- Can assist you to transfer sensitive information safely to us. + +To speed up the communication with customer support, consider the following: + +- Whenever is possible, use the in-app bug report feature. It provides an application specific guide compared to using the generic report form on web. +- Whenever is possible, proactively attach logs to your report. Reporting an issue from the application can help you in that. +- Check whether your system is officially supported by Proton, including the source of the installer. We cannot provide help when the application is packaged by a third party or when the application is used on systems that we do not prepare to support. +- If your report is a feature request, see the Feature request section. In case it is an issue related to application security, see the Security vulnerabilities section. + +In the past, we used GitHub issue tracker for more technical issues in parallel to Proton customer support, but we run into limitations with this approach: + +- Monitoring GitHub issue tracker took development time as it was managed by the development team. +- It made issue frequency tracking challenging because we did not have a single point of entry for issues. +- Users were confused what technical issue means, and used the GitHub issue tracker for feature requests, or non-technical discussions. +- Users sometimes shared sensitive data through the GitHub issue tracker. + +For the above reasons, we do not use GitHub issue tracker anymore but ask you to contact our customer support in case you run into a problem. + +### Security vulnerabilities + +Proton runs a bug bounty program for security vulnerabilities. They differ from normal bug reports in the following ways: + +- These reports go directly to our security team. +- They expect deeper explanation of the issue. +- Depending on the finding, they may be financially rewarded. + +More information about the program can be found [here](https://proton.me/security/bug-bounty). + +## Feature requests + +What someone considers as a bug is sometimes a feature, and sometimes, a missing feature is considered as a bug. Instead of reporting feature requests as bugs, we setup a UserVoice page to allow our users to share their preferences. UserVoice also makes it possible to vote on other feature requests, making the community preference public. + +Our product team frequently monitors UserVoice, and the features listed there are taken into account in our planning. + +Examples for UserVoice requests: + +- Extending the officially supported environments (e.g., operating systems, clients, or computer architectures). +- Requesting new features. +- Integration with non-Proton services. + +UserVoice is available [here](https://protonmail.uservoice.com/). + +## Asking for help + +The best ways to get answer for generic questions or to get help with setting up the system is to interact with our active community on [Reddit](https://reddit.com/r/ProtonMail/) or to contact customer support. + +## Code contribution + +We are grateful if you can contribute directly with code. In that case there is nothing else to do than to open a pull request. + +The following is worthwhile noting + +- The project is primarily developed on an internal repository, and the one on GitHub is only a mirror of it. For that reason, the merge request will not be merged on GitHub but added to the project internally. We are keeping the original author in the change set to respect the contribution. +- The application is used on numerous platforms and by many third party clients. To have higher chance your change to be accepted, consider all supported dependencies. +- Give detailed description of the issue, preferably with test steps to reproduce the original issue, and to verify the fix. It is even better if you also extend the automated tests. + +### Contribution policy By making a contribution to this project: -1. I assign any and all copyright related to the contribution to Proton AG; -2. I certify that the contribution was created in whole by me; -3. I understand and agree that this project and the contribution are public - and that a record of the contribution (including all personal information I - submit with it) is maintained indefinitely and may be redistributed with - this project or the open source license(s) involved. +1. You assign any and all copyright related to the contribution to Proton AG; +2. You certify that the contribution was created in whole by you; +3. You understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information you submit with it) is maintained indefinitely and may be redistributed with this project or the open source license(s) involved.