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/.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
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.
diff --git a/go.mod b/go.mod
index aa8aac2a..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.20241018144126-31e040c2417e
+ 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 e936d2d0..9d5e7a20 100644
--- a/go.sum
+++ b/go.sum
@@ -34,20 +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.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/app/app.go b/internal/app/app.go
index edc7697a..4aa3e4c7 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -208,6 +208,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,
}
diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go
index 63387292..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
}
@@ -723,3 +715,18 @@ 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")
+ }
+}
+
+// GetUsers is only used for testing purposes.
+func (bridge *Bridge) GetUsers() map[string]*user.User {
+ return bridge.users
+}
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/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/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/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..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"
@@ -255,7 +256,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 +281,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 +359,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 +528,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 +542,6 @@ func (bridge *Bridge) addUserWithVault(
bridge.panicHandler,
bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(),
- statsPath,
bridge,
bridge.serverManager,
bridge.serverManager,
@@ -589,9 +584,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()
@@ -610,26 +608,21 @@ 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.
-func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
+// 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())
- // if this is actually a remove account
- if withData && withAPI {
- user.SendConfigStatusAbort(ctx, withTelemetry)
- }
-
logUser.WithFields(logrus.Fields{
"userID": user.ID(),
"withAPI": 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")
}
- bridge.heartbeat.SetNbAccount(len(bridge.users))
+ bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
user.Close()
}
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..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.";
}
@@ -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/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/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/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/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.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/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/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
}
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/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/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/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..99d18788 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),
@@ -242,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})
@@ -371,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)
@@ -513,7 +515,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 +532,6 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.addressMode,
s.sendRecorder,
s.panicHandler,
- s.telemetry,
s.reporter,
s.showAllMail,
s.syncStateProvider,
@@ -655,6 +655,8 @@ type onLogoutReq struct{}
type showAllMailReq struct{ v bool }
+type onDeleteReq struct{}
+
type setAddressModeReq struct {
mode usertypes.AddressMode
}
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/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)"),
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/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/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/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
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/telemetry/heartbeat.go b/internal/telemetry/heartbeat.go
index 0c9649c1..fa492125 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"
)
@@ -31,71 +34,67 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke
log: logrus.WithField("pkg", "telemetry"),
manager: manager,
metrics: HeartbeatData{
- MeasurementGroup: "bridge.any.usage",
- Event: "bridge_heartbeat",
+ MeasurementGroup: "bridge.any.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..77cf526f 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",
+ MeasurementGroup: "bridge.any.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/unleash/service.go b/internal/unleash/service.go
index 174cd639..0ae4fb48 100644
--- a/internal/unleash/service.go
+++ b/internal/unleash/service.go
@@ -36,6 +36,12 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
const filename = "unleash_flags"
+const (
+ EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
+ IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
+ UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
+)
+
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
type GetFlagValueFn func(key string) bool
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..536c1b3a 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"
@@ -65,6 +63,8 @@ type User struct {
id string
log *logrus.Entry
+ userPlan string
+
vault *vault.User
client *proton.Client
reporter reporter.Reporter
@@ -78,10 +78,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 +101,6 @@ func New(
crashHandler async.PanicHandler,
showAllMail bool,
maxSyncMemory uint64,
- statsDir string,
telemetryManager telemetry.Availability,
imapServerManager imapservice.IMAPServerManager,
smtpServerManager smtp.ServerManager,
@@ -125,7 +121,6 @@ func New(
crashHandler,
showAllMail,
maxSyncMemory,
- statsDir,
telemetryManager,
imapServerManager,
smtpServerManager,
@@ -159,7 +154,6 @@ func newImpl(
crashHandler async.PanicHandler,
showAllMail bool,
maxSyncMemory uint64,
- statsDir string,
telemetryManager telemetry.Availability,
imapServerManager imapservice.IMAPServerManager,
smtpServerManager smtp.ServerManager,
@@ -184,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 {
@@ -198,12 +200,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.
@@ -211,6 +207,8 @@ func newImpl(
log: logrus.WithField("userID", apiUser.ID),
id: apiUser.ID,
+ userPlan: userPlan,
+
vault: encVault,
client: client,
reporter: reporter,
@@ -225,7 +223,6 @@ func newImpl(
panicHandler: crashHandler,
- configStatus: configStatus,
telemetryManager: telemetryManager,
serviceGroup: orderedtasks.NewOrderedCancelGroup(crashHandler),
@@ -248,7 +245,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 +257,6 @@ func newImpl(
reporter,
encVault,
encVault,
- user,
user.eventService,
addressMode,
identityState.Clone(),
@@ -279,7 +275,6 @@ func newImpl(
encVault,
crashHandler,
sendRecorder,
- user,
reporter,
addressMode,
eventSubscription,
@@ -291,12 +286,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) {
@@ -340,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)
@@ -439,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")
@@ -598,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")
@@ -607,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()
@@ -698,19 +709,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/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/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])
}
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/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
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/features/bridge/heartbeat.feature b/tests/features/bridge/heartbeat.feature
index 1288b7c5..ba225fef 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",
+ "MeasurementGroup": "bridge.any.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"
}
}
"""
@@ -58,26 +62,28 @@ Feature: Send Telemetry Heartbeat
Then bridge eventually sends the following heartbeat:
"""
{
- "MeasurementGroup": "bridge.any.usage",
- "Event": "bridge_heartbeat",
+ "MeasurementGroup": "bridge.any.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"
}
}
"""
@@ -96,26 +102,106 @@ Feature: Send Telemetry Heartbeat
Then bridge eventually sends the following heartbeat:
"""
{
- "MeasurementGroup": "bridge.any.usage",
- "Event": "bridge_heartbeat",
+ "MeasurementGroup": "bridge.any.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.heartbeat",
+ "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.heartbeat",
+ "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/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/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])"
+
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/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/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 58bf1358..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)
@@ -129,13 +130,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)
@@ -201,15 +208,10 @@ 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)
- 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)
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)